Files
darling-lol/patches/xnu.patch
T
lucannez64 bdbe76fd48 Handle SIOCGIFMEDIA in the ioctl shim
Vivox's network reachability provider calls SIOCGIFMEDIA for each interface and drops every interface whose ioctl fails. Darling passed the Darwin request through to Linux, which returns ENOTTY, so the SDK saw no usable interface and vx_req_connector_create failed with 1004. Return a synthetic active Ethernet/IEEE802.11 media record built from the host SIOCGIFFLAGS result, and add a standalone probe that fails on the old behavior.
2026-10-19 20:24:19 +02:00

6283 lines
250 KiB
Diff

diff --git a/darling/src/libsystem_kernel/CMakeLists.txt b/darling/src/libsystem_kernel/CMakeLists.txt
index e14e7ea..024981b 100644
--- a/darling/src/libsystem_kernel/CMakeLists.txt
+++ b/darling/src/libsystem_kernel/CMakeLists.txt
@@ -40,7 +40,11 @@ add_circular(system_kernel FAT
$<IF:$<BOOL:${BUILD_TARGET_64BIT}>,$<TARGET_OBJECTS:libsyscall_64>,>
$<TARGET_OBJECTS:libsyscall>
$<TARGET_OBJECTS:libsyscall_dynamic>
- #$<TARGET_OBJECTS:kqueue>
+ # darling-lol: link kqueue emulation (see libkqueue/CMakeLists.txt).
+ # Without this, kqueue()/kevent() are ENOSYS weak stubs and all
+ # dispatch-source / FilePathWatcher / socket-server event delivery
+ # silently never fires.
+ $<TARGET_OBJECTS:kqueue>
$<TARGET_OBJECTS:emulation>
SIBLINGS
system_c
@@ -48,6 +52,12 @@ add_circular(system_kernel FAT
system_dyld
DEPENDENCIES
libsimple_darling
+ # darling-lol: kqueue emulation objects (see OBJECTS above) use
+ # calloc/pthread, which live in upper layers. UPWARD defers binding
+ # to runtime instead of failing the link (or creating a cycle).
+ UPWARD
+ system_malloc
+ system_pthread
)
#target_link_libraries(system_kernel system_duct platform_static32 platform_static64)
make_fat(system_kernel)
diff --git a/darling/src/libsystem_kernel/emulation/CMakeLists.txt b/darling/src/libsystem_kernel/emulation/CMakeLists.txt
index 5b2e765..40308eb 100644
--- a/darling/src/libsystem_kernel/emulation/CMakeLists.txt
+++ b/darling/src/libsystem_kernel/emulation/CMakeLists.txt
@@ -6,13 +6,12 @@ enable_language(ASM)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 -fvisibility=hidden -fPIC -fno-builtin -ggdb -Wno-int-conversion -Wno-compare-distinct-pointer-types")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -nostdlib")
-add_definitions(-DBSDTHREAD_WRAP_LINUX_PTHREAD
- -DEMULATED_SYSNAME="Darwin"
- -DEMULATED_RELEASE="20.6.0"
- -DEMULATED_VERSION="Darwin Kernel Version 20.6.0"
- -DEMULATED_OSVERSION="20G1120"
- -DEMULATED_OSPRODUCTVERSION="11.7.4"
-)
+add_definitions(-DBSDTHREAD_WRAP_LINUX_PTHREAD)
+
+# Quoted values are kept off the directory-wide definitions: those are pasted unescaped
+# into the mig command lines, which breaks the shell quoting.
+set_source_files_properties(src/xnu_syscall/bsd/helper/misc/sysctl_kern.c PROPERTIES COMPILE_DEFINITIONS
+ "EMULATED_SYSNAME=\"Darwin\";EMULATED_RELEASE=\"23.6.0\";EMULATED_VERSION=\"Darwin Kernel Version 23.6.0: Mon Jul 29 21:14:30 PDT 2024\\; root:xnu-10063.141.2~1/RELEASE_X86_64\";EMULATED_OSVERSION=\"23H124\";EMULATED_OSPRODUCTVERSION=\"14.7\"")
# include src/startup for rtsig.h
include_directories(
@@ -134,6 +133,7 @@ set(xnu_syscall_bsd_sources
src/xnu_syscall/bsd/impl/guarded/guarded_close_np.c
src/xnu_syscall/bsd/impl/guarded/guarded_kqueue_np.c
src/xnu_syscall/bsd/impl/guarded/guarded_open_np.c
+ src/xnu_syscall/bsd/impl/guarded/change_fdguard_np.c
src/xnu_syscall/bsd/impl/hfs/stub.c
src/xnu_syscall/bsd/impl/ioctl/ioctl.c
src/xnu_syscall/bsd/impl/kqueue/kevent.c
@@ -153,6 +153,7 @@ set(xnu_syscall_bsd_sources
src/xnu_syscall/bsd/impl/misc/getrlimit.c
src/xnu_syscall/bsd/impl/misc/getrusage.c
src/xnu_syscall/bsd/impl/misc/iopolicysys.c
+ src/xnu_syscall/bsd/impl/misc/mac_syscall.c
src/xnu_syscall/bsd/impl/misc/proc_info.c
src/xnu_syscall/bsd/impl/misc/ptrace.c
src/xnu_syscall/bsd/impl/misc/reboot.c
@@ -359,6 +360,11 @@ set(emulation_sources
${CMAKE_BINARY_DIR}/src/external/darlingserver/src/rpc.c
)
+# darling-lol: keep these at -O0 even when the tree is built optimized. bsdthread_register.c
+# sets %rsp from the thread argument in inline assembly and sigexc.c runs on the fault and
+# suspend signal stacks; both depend on the unoptimized stack layout.
+set_source_files_properties(src/xnu_syscall/bsd/impl/bsdthread/bsdthread_register.c
+ src/linux_premigration/signal/sigexc.c PROPERTIES COMPILE_OPTIONS "-O0")
set_source_files_properties(src/legacy_path/signal/duct_signals.c PROPERTIES COMPILE_FLAGS "-nostdinc")
set_source_files_properties(src/legacy_path/signal/sigexc.c PROPERTIES OBJECT_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/src/legacy_path/signal/exc.h)
set_source_files_properties(${CMAKE_BINARY_DIR}/src/external/darlingserver/src/rpc.c PROPERTIES
diff --git a/darling/src/libsystem_kernel/emulation/include/common/base.h b/darling/src/libsystem_kernel/emulation/include/common/base.h
index 207dae6..f5e96db 100644
--- a/darling/src/libsystem_kernel/emulation/include/common/base.h
+++ b/darling/src/libsystem_kernel/emulation/include/common/base.h
@@ -11,13 +11,13 @@
#define CPP_EXTERN_END
#endif
-#define LINUX_SYSCALL0(nr) linux_syscall(0, 0, 0, 0, 0, 0, nr)
-#define LINUX_SYSCALL1(nr, a1) linux_syscall((long)a1, 0, 0, 0, 0, 0, nr)
-#define LINUX_SYSCALL2(nr, a1, a2) linux_syscall((long)a1, (long)a2, 0, 0, 0, 0, nr)
-#define LINUX_SYSCALL3(nr, a1, a2, a3) linux_syscall((long)a1, (long)a2, (long)a3, 0, 0, 0, nr)
-#define LINUX_SYSCALL4(nr, a1, a2, a3, a4) linux_syscall((long)a1, (long)a2, (long)a3, (long)a4, 0, 0, nr)
-#define LINUX_SYSCALL5(nr, a1, a2, a3, a4, a5) linux_syscall((long)a1, (long)a2, (long)a3, (long)a4, (long)a5, 0, nr)
-#define LINUX_SYSCALL6(nr, a1, a2, a3, a4, a5, a6) linux_syscall((long)a1, (long)a2, (long)a3, (long)a4, (long)a5, (long)a6, nr)
+#define LINUX_SYSCALL0(nr) __lolsys_syscall(0, 0, 0, 0, 0, 0, nr)
+#define LINUX_SYSCALL1(nr, a1) __lolsys_syscall((long)a1, 0, 0, 0, 0, 0, nr)
+#define LINUX_SYSCALL2(nr, a1, a2) __lolsys_syscall((long)a1, (long)a2, 0, 0, 0, 0, nr)
+#define LINUX_SYSCALL3(nr, a1, a2, a3) __lolsys_syscall((long)a1, (long)a2, (long)a3, 0, 0, 0, nr)
+#define LINUX_SYSCALL4(nr, a1, a2, a3, a4) __lolsys_syscall((long)a1, (long)a2, (long)a3, (long)a4, 0, 0, nr)
+#define LINUX_SYSCALL5(nr, a1, a2, a3, a4, a5) __lolsys_syscall((long)a1, (long)a2, (long)a3, (long)a4, (long)a5, 0, nr)
+#define LINUX_SYSCALL6(nr, a1, a2, a3, a4, a5, a6) __lolsys_syscall((long)a1, (long)a2, (long)a3, (long)a4, (long)a5, (long)a6, nr)
#define __SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n
#define __SYSCALL_NARGS(...) __SYSCALL_NARGS_X(__VA_ARGS__,7,6,5,4,3,2,1,0)
@@ -27,6 +27,9 @@
long linux_syscall(long a1, long a2, long a3, long a4, long a5, long a6, int nr);
+/* darling-lol M58x: traced wrapper the LINUX_SYSCALL* macros route through */
+long __lolsys_syscall(long a1, long a2, long a3, long a4, long a5, long a6, int nr);
+
#ifndef BUILDING_BASE_C
VISIBLE
diff --git a/darling/src/libsystem_kernel/emulation/include/common/cpu_topology.h b/darling/src/libsystem_kernel/emulation/include/common/cpu_topology.h
new file mode 100644
index 0000000..8ae1665
--- /dev/null
+++ b/darling/src/libsystem_kernel/emulation/include/common/cpu_topology.h
@@ -0,0 +1,272 @@
+#ifndef DARLING_COMMON_CPU_TOPOLOGY_H
+#define DARLING_COMMON_CPU_TOPOLOGY_H
+
+#include <stdbool.h>
+
+#include <darling/emulation/common/base.h>
+#include <darling/emulation/common/simple.h>
+#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
+
+/*
+ * darling-lol: the host's real CPU topology for the hw.* / machdep.cpu.* sysctls.
+ *
+ * The guest used to be told the fixed MacBookPro16,1 configuration (8 logical
+ * and 8 physical CPUs). The game and the clients size their worker pools from
+ * hw.ncpu / hw.activecpu, so on this 6-core/12-thread host the game created 7
+ * workers where the same PC runs 12 threads under Windows. Report what the
+ * host really has instead, while the emulated Mac identity (hw.model, brand
+ * string) stays unchanged.
+ *
+ * Values are cached once per process. avail/active reflect the process's
+ * affinity mask (cgroup or taskset restrictions); max values reflect the
+ * online CPUs. The host flag file /tmp/rclol-fixed-cpu-count (checked once
+ * with a raw access() syscall) restores the old fixed values for A/B runs.
+ */
+
+/* From src/xnu_syscall/bsd/helper/misc/sysctl_proc.c */
+bool read_string(const char* path, char* dst, int maxlen);
+
+#define DARLING_CPU_FIXED_COUNT 8
+#define DARLING_CPU_MASK_BYTES 128 /* 1024 CPUs, enough for any League host */
+#define DARLING_CPU_MAX_SCAN 256
+
+/* The A/B opt-out flag file; the host check overrides the path. */
+#ifndef DARLING_CPU_FIXED_FLAG
+#define DARLING_CPU_FIXED_FLAG "/tmp/rclol-fixed-cpu-count"
+#endif
+
+struct darling_cpu_topology
+{
+ unsigned int online; /* CPUs online on the host (hw.ncpu) */
+ unsigned int available; /* CPUs this process may run on (hw.activecpu) */
+ unsigned int physical_online; /* physical cores among the online CPUs */
+ unsigned int physical_available; /* physical cores among the available CPUs */
+ bool fixed; /* the opt-out flag file is present */
+ bool detected; /* the host answered; false means "use the old 8s" */
+};
+
+/* Number of CPUs in a sysfs list such as "0-1,4-5" or "0,6" */
+static inline unsigned int darling_cpu_count_list(const char* list)
+{
+ unsigned int count = 0;
+ const char* p = list;
+
+ for (;;)
+ {
+ unsigned long long first = 0, last;
+ const char* start;
+
+ while (*p == ',' || *p == ' ' || *p == '\t' || *p == '\n')
+ ++p;
+ start = p;
+ while (*p >= '0' && *p <= '9')
+ first = first * 10 + (unsigned int)(*p++ - '0');
+ if (p == start)
+ break;
+
+ last = first;
+ if (*p == '-')
+ {
+ ++p;
+ last = 0;
+ while (*p >= '0' && *p <= '9')
+ last = last * 10 + (unsigned int)(*p++ - '0');
+ }
+
+ if (last >= first && last - first < 4096)
+ count += (unsigned int)(last - first + 1);
+
+ if (*p != ',' && *p != ' ' && *p != '\t' && *p != '\n')
+ break;
+ }
+
+ return count;
+}
+
+static inline bool darling_cpu_mask_has(const unsigned char* mask, long mask_bytes, unsigned int cpu)
+{
+ long byte = (long)(cpu >> 3);
+
+ if (byte >= mask_bytes)
+ return false;
+ return (mask[byte] & (1u << (cpu & 7))) != 0;
+}
+
+static inline unsigned int darling_cpu_mask_count(const unsigned char* mask, long mask_bytes)
+{
+ unsigned int count = 0;
+
+ for (long i = 0; i < mask_bytes; ++i)
+ count += (unsigned int)__builtin_popcount(mask[i]);
+
+ return count;
+}
+
+/*
+ * Count distinct (physical_package_id, core_id) pairs among the CPUs in
+ * [0, online). With a mask, only the CPUs it selects are counted and their
+ * pair count is returned through available_cores. Returns the total pair
+ * count, or 0 when the host does not expose the topology (the caller then
+ * falls back to an SMT estimate).
+ */
+static inline unsigned int darling_cpu_scan_cores(const unsigned char* mask, long mask_bytes,
+ unsigned int online, unsigned int* available_cores)
+{
+ unsigned long long pairs[DARLING_CPU_MAX_SCAN], available_pairs[DARLING_CPU_MAX_SCAN];
+ unsigned int pair_count = 0, available_count = 0;
+
+ if (available_cores != NULL)
+ *available_cores = 0;
+ if (online > DARLING_CPU_MAX_SCAN)
+ return 0;
+
+ for (unsigned int cpu = 0; cpu < online; ++cpu)
+ {
+ char path[112], buf[32];
+ unsigned long long package, core, pair;
+ bool seen = false;
+
+ __simple_sprintf(path, "/sys/devices/system/cpu/cpu%u/topology/physical_package_id", cpu);
+ if (!read_string(path, buf, sizeof(buf)))
+ return 0;
+ package = __simple_atoi(buf, NULL);
+
+ __simple_sprintf(path, "/sys/devices/system/cpu/cpu%u/topology/core_id", cpu);
+ if (!read_string(path, buf, sizeof(buf)))
+ return 0;
+ core = __simple_atoi(buf, NULL);
+
+ pair = (package << 32) | (core & 0xffffffffull);
+
+ for (unsigned int i = 0; i < pair_count; ++i)
+ {
+ if (pairs[i] == pair)
+ {
+ seen = true;
+ break;
+ }
+ }
+ if (!seen)
+ pairs[pair_count++] = pair;
+
+ if (mask != NULL && darling_cpu_mask_has(mask, mask_bytes, cpu))
+ {
+ seen = false;
+ for (unsigned int i = 0; i < available_count; ++i)
+ {
+ if (available_pairs[i] == pair)
+ {
+ seen = true;
+ break;
+ }
+ }
+ if (!seen)
+ available_pairs[available_count++] = pair;
+ }
+ }
+
+ if (available_cores != NULL)
+ *available_cores = available_count;
+ return pair_count;
+}
+
+/* SMT estimate from cpu0's sibling list, used when the per-CPU scan fails. */
+static inline unsigned int darling_cpu_smt_estimate(unsigned int online)
+{
+ char buf[128];
+ unsigned int siblings;
+
+ if (!read_string("/sys/devices/system/cpu/cpu0/topology/thread_siblings_list", buf, sizeof(buf)))
+ return 0;
+
+ siblings = darling_cpu_count_list(buf);
+ if (siblings == 0 || online % siblings != 0)
+ return 0;
+ return online / siblings;
+}
+
+static inline void darling_cpu_topology_probe(struct darling_cpu_topology* topo)
+{
+ unsigned char mask[DARLING_CPU_MASK_BYTES];
+ char buf[512];
+ long mask_bytes;
+
+ *topo = (struct darling_cpu_topology) { 0 };
+
+ /* The A/B opt-out is a host flag file, like the other darling-lol opt-ins. */
+ if (LINUX_SYSCALL(__NR_access, DARLING_CPU_FIXED_FLAG, 0) == 0)
+ {
+ topo->fixed = true;
+ return;
+ }
+
+ if (read_string("/sys/devices/system/cpu/online", buf, sizeof(buf)))
+ topo->online = darling_cpu_count_list(buf);
+ else if (read_string("/sys/devices/system/cpu/present", buf, sizeof(buf)))
+ topo->online = darling_cpu_count_list(buf);
+
+ mask_bytes = LINUX_SYSCALL(__NR_sched_getaffinity, 0, (long)sizeof(mask), mask);
+ if (mask_bytes > 0)
+ {
+ if (mask_bytes > (long)sizeof(mask))
+ mask_bytes = (long)sizeof(mask);
+ topo->available = darling_cpu_mask_count(mask, mask_bytes);
+ }
+ else
+ {
+ mask_bytes = 0;
+ }
+
+ if (topo->online == 0)
+ topo->online = topo->available;
+ if (topo->available == 0)
+ topo->available = topo->online;
+ if (topo->online == 0)
+ return; /* detection failed: the caller keeps the fixed 8s */
+
+ if (topo->available > topo->online)
+ topo->available = topo->online;
+
+ topo->physical_online = darling_cpu_scan_cores(NULL, 0, topo->online, NULL);
+ if (topo->physical_online == 0)
+ topo->physical_online = darling_cpu_smt_estimate(topo->online);
+ /* libmalloc divides logical by physical and only accepts ratios 1/2/4. */
+ if (topo->physical_online == 0 || topo->online % topo->physical_online != 0)
+ topo->physical_online = topo->online;
+
+ if (mask_bytes > 0)
+ {
+ unsigned int available_cores = 0;
+ unsigned int total = darling_cpu_scan_cores(mask, mask_bytes, topo->online, &available_cores);
+
+ topo->physical_available = (total > 0) ? available_cores : topo->physical_online;
+ }
+ else
+ {
+ topo->physical_available = topo->physical_online;
+ }
+ if (topo->physical_available > topo->physical_online)
+ topo->physical_available = topo->physical_online;
+
+ topo->detected = true;
+}
+
+/* Cached for the life of the process; the probe runs at most once per copy. */
+static inline const struct darling_cpu_topology* darling_host_cpu_topology(void)
+{
+ static struct darling_cpu_topology cached;
+ static int decided;
+
+ if (__atomic_load_n(&decided, __ATOMIC_ACQUIRE) == 0)
+ {
+ struct darling_cpu_topology probe;
+
+ darling_cpu_topology_probe(&probe);
+ cached = probe;
+ __atomic_store_n(&decided, 1, __ATOMIC_RELEASE);
+ }
+
+ return &cached;
+}
+
+#endif // DARLING_COMMON_CPU_TOPOLOGY_H
diff --git a/darling/src/libsystem_kernel/emulation/include/common/private_mutex.h b/darling/src/libsystem_kernel/emulation/include/common/private_mutex.h
new file mode 100644
index 0000000..5379dd4
--- /dev/null
+++ b/darling/src/libsystem_kernel/emulation/include/common/private_mutex.h
@@ -0,0 +1,20 @@
+#ifndef DARLING_EMULATION_PRIVATE_MUTEX_H
+#define DARLING_EMULATION_PRIVATE_MUTEX_H
+#include <darling/emulation/linux_premigration/ext/futex.h>
+/* Bootstrap-safe private mutex. Kernel emulation must not export placeholder
+ * os_unfair_lock functions: umbrella lookup can bind app calls to those stubs
+ * instead of libplatform, silently removing allocator mutual exclusion. */
+typedef struct { int value; } emulation_mutex;
+#define EMULATION_MUTEX_INITIALIZER {0}
+static inline void emulation_mutex_lock(emulation_mutex *mutex) {
+ int expected = 0;
+ if (__atomic_compare_exchange_n(&mutex->value, &expected, 1, 0,
+ __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) return;
+ while (__atomic_exchange_n(&mutex->value, 2, __ATOMIC_ACQUIRE) != 0)
+ __linux_futex_reterr(&mutex->value, FUTEX_WAIT | 128, 2, 0, 0, 0);
+}
+static inline void emulation_mutex_unlock(emulation_mutex *mutex) {
+ if (__atomic_exchange_n(&mutex->value, 0, __ATOMIC_RELEASE) == 2)
+ __linux_futex_reterr(&mutex->value, FUTEX_WAKE | 128, 1, 0, 0, 0);
+}
+#endif
diff --git a/darling/src/libsystem_kernel/emulation/include/common/slowwait.h b/darling/src/libsystem_kernel/emulation/include/common/slowwait.h
new file mode 100644
index 0000000..c310159
--- /dev/null
+++ b/darling/src/libsystem_kernel/emulation/include/common/slowwait.h
@@ -0,0 +1,55 @@
+#ifndef _DARLING_SLOWWAIT_H_
+#define _DARLING_SLOWWAIT_H_
+/* darling-lol diagnostic: with /tmp/metal-debug-timing present, log main-thread blocking waits over 400 ms. */
+#include <darling/emulation/common/base.h>
+#include <darling/emulation/common/simple.h>
+#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
+
+/* darling-lol diagnostic: with /tmp/metal-debug-timing present, log mach semaphore waits that block for more than
+ * 400 ms, with the caller's frame-pointer chain, to /tmp/semwait.log. */
+static inline int slowwait_enabled(void)
+{
+ static int state = -1;
+ if (state < 0) {
+ state = LINUX_SYSCALL(__NR_access, "/tmp/metal-debug-timing", 0) == 0 ? 1 : 0;
+ }
+ return state;
+}
+
+static inline unsigned long long slowwait_now(void)
+{
+ struct { long tv_sec; long tv_nsec; } ts;
+ LINUX_SYSCALL(__NR_clock_gettime, 1, &ts);
+ return (unsigned long long) ts.tv_sec * 1000000000ULL + (unsigned long long) ts.tv_nsec;
+}
+
+static inline void slowwait_log(const char* kind, unsigned int name, unsigned long long start, void** frame, unsigned int extra1, unsigned int extra2)
+{
+ unsigned long long elapsed = slowwait_now() - start;
+ /* only the main thread (tid == pid), the frame loop; other threads idle in these waits by design */
+ if (elapsed < 400ULL * 1000000ULL || LINUX_SYSCALL(__NR_gettid) != LINUX_SYSCALL(__NR_getpid)) {
+ return;
+ }
+ char line[512];
+ int n = __simple_sprintf(line, "%s name=%u x1=%x x2=%u ms=%llu tid=%d frames:", kind, name, extra1, extra2, elapsed / 1000000ULL, (int) LINUX_SYSCALL(__NR_gettid));
+ void** fp = frame;
+ for (int i = 0; i < 10 && fp && ((unsigned long) fp & 7) == 0; ++i) {
+ void* ret = fp[1];
+ n += __simple_sprintf(line + n, " %lx", (unsigned long) ret);
+ void** next = (void**) fp[0];
+ if (next <= fp || (unsigned long) next - (unsigned long) fp > 0x100000) {
+ break;
+ }
+ fp = next;
+ }
+ n += __simple_sprintf(line + n, "\n");
+ long fd = LINUX_SYSCALL(__NR_open, "/tmp/semwait.log", 02101 /* O_WRONLY|O_CREAT|O_APPEND */, 0666);
+ if (fd >= 0) {
+ LINUX_SYSCALL(__NR_write, fd, line, n);
+ LINUX_SYSCALL(__NR_close, fd);
+ }
+}
+
+
+
+#endif
diff --git a/darling/src/libsystem_kernel/emulation/include/conversion/xattr/name.h b/darling/src/libsystem_kernel/emulation/include/conversion/xattr/name.h
new file mode 100644
index 0000000..976e5d0
--- /dev/null
+++ b/darling/src/libsystem_kernel/emulation/include/conversion/xattr/name.h
@@ -0,0 +1,69 @@
+#ifndef DARLING_CONVERSION_XATTR_NAME
+#define DARLING_CONVERSION_XATTR_NAME
+
+#include <darling/emulation/conversion/errno.h>
+
+/*
+ * darling-lol: macOS xattr names have no namespace prefix
+ * (e.g. "com.apple.quarantine", "org.chromium.crashpad.database.initialized").
+ * Linux requires the "user." namespace for names an unprivileged process may
+ * set/get, and returns ENOTSUP (95) otherwise. Darwin's own xattr layer is
+ * namespace-less, so translate on the way in/out.
+ *
+ * Rules:
+ * - "" (Darwin: list/remove all) is passed through unchanged.
+ * - names already carrying a Linux namespace ("user.", "trusted.",
+ * "system.", "security.") are passed through (avoid double-prefixing).
+ * - everything else gets "user." prepended.
+ *
+ * OUT must have room for 5 extra bytes + NUL. Returns `name` or `out`.
+ */
+static inline const char* xattr_name_to_linux(const char* name, char* out, unsigned long outsz)
+{
+ unsigned long i = 0;
+
+ if (name == 0 || name[0] == '\0')
+ return name;
+
+ if (__builtin_strncmp(name, "user.", 5) == 0 ||
+ __builtin_strncmp(name, "trusted.", 8) == 0 ||
+ __builtin_strncmp(name, "system.", 7) == 0 ||
+ __builtin_strncmp(name, "security.", 9) == 0)
+ return name;
+
+ if (out == 0 || outsz < 6)
+ return name;
+
+ out[0] = 'u'; out[1] = 's'; out[2] = 'e'; out[3] = 'r'; out[4] = '.';
+ for (i = 0; name[i] != '\0' && i + 5 < outsz - 1; i++)
+ out[i + 5] = name[i];
+ out[i + 5] = '\0';
+ return out;
+}
+
+/*
+ * Reverse: strip a leading "user." when returning names to Darwin callers
+ * (listxattr). Mutates `name` in place (the buffer is caller-owned and the
+ * caller only needs a valid NUL-terminated name at the same address).
+ * Returns the (possibly advanced) pointer for convenience.
+ */
+static inline char* xattr_name_from_linux(char* name)
+{
+ if (name != 0 && __builtin_strncmp(name, "user.", 5) == 0)
+ return name + 5;
+ return name;
+}
+
+/*
+ * Linux reports a missing attribute as ENODATA; Darwin reports ENOATTR (93),
+ * which callers test for to tell "no such attribute" from a real failure.
+ * LINUX_ERRNO is the negative value returned by the Linux xattr syscall.
+ */
+static inline long xattr_errno_to_bsd(long linux_errno)
+{
+ if (linux_errno == -61 /* LINUX_ENODATA */)
+ return -93 /* ENOATTR */;
+ return errno_linux_to_bsd(linux_errno);
+}
+
+#endif /* DARLING_CONVERSION_XATTR_NAME */
diff --git a/darling/src/libsystem_kernel/emulation/include/linux_premigration/resources/dserver-rpc-defs.h b/darling/src/libsystem_kernel/emulation/include/linux_premigration/resources/dserver-rpc-defs.h
index f6588cc..c60d8f9 100644
--- a/darling/src/libsystem_kernel/emulation/include/linux_premigration/resources/dserver-rpc-defs.h
+++ b/darling/src/libsystem_kernel/emulation/include/linux_premigration/resources/dserver-rpc-defs.h
@@ -12,6 +12,7 @@
#include <darling/emulation/linux_premigration/elfcalls_wrapper.h>
#include <darling/emulation/common/simple.h>
#include <darling/emulation/xnu_syscall/bsd/impl/signal/sigprocmask.h>
+#include <darling/emulation/conversion/signal/duct_signals.h>
#include <darling/emulation/xnu_syscall/bsd/impl/unistd/close.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
@@ -75,7 +76,67 @@ struct linux_sockaddr_un {
#define dserver_rpc_hooks_memcpy memcpy
+#include <darling/emulation/common/slowwait.h>
+
+/* darling-lol diagnostic (/tmp/metal-debug-timing): time main-thread RPCs to darlingserver */
+static int slowrpc_pending_call = -1;
+static unsigned long long slowrpc_start = 0;
+
+/* darling-lol diagnostic (host file /tmp/rclol-rpc-trace): every 16th darlingserver request from a process's main thread is
+ * appended to /tmp/rclol-rpc-trace.log with its call number and up to 10 caller addresses (frame-pointer walk). Offline,
+ * the addresses are resolved with /proc/<pid>/maps to attribute the ~40 round trips per frame to code. */
+static int rclol_rpc_trace_state = -1;
+static unsigned long rclol_rpc_trace_count = 0;
+
+static void rclol_rpc_trace_sample(const dserver_rpc_hooks_msghdr_t* message, void** frame) {
+ char line[400];
+ int n, call;
+ if (rclol_rpc_trace_state < 0)
+ rclol_rpc_trace_state = LINUX_SYSCALL(__NR_access, "/tmp/rclol-rpc-trace", 0) == 0;
+ if (!rclol_rpc_trace_state || message == 0 || message->msg_iov == 0 || message->msg_iovlen < 1 || message->msg_iov->iov_base == 0 ||
+ message->msg_iov->iov_len < sizeof(int))
+ return;
+ if (LINUX_SYSCALL(__NR_gettid) != LINUX_SYSCALL(__NR_getpid))
+ return;
+ if ((__atomic_add_fetch(&rclol_rpc_trace_count, 1, __ATOMIC_RELAXED) & 15) != 0)
+ return;
+ call = *(const int*) message->msg_iov->iov_base;
+ n = __simple_sprintf(line, "rpc pid=%d call=%d frames:", (int) LINUX_SYSCALL(__NR_getpid), call & 0xffff);
+ for (int i = 0; i < 10 && frame && ((unsigned long) frame & 7) == 0; ++i) {
+ void** next = (void**) frame[0];
+ n += __simple_sprintf(line + n, " %lx", (unsigned long) frame[1]);
+ if (next <= frame || (unsigned long) next - (unsigned long) frame > 0x100000)
+ break;
+ frame = next;
+ }
+ line[n++] = '\n';
+ line[n] = 0;
+ long fd = LINUX_SYSCALL(__NR_open, "/tmp/rclol-rpc-trace.log", 02101, 0666);
+ if (fd >= 0) {
+ LINUX_SYSCALL(__NR_write, fd, line, n);
+ LINUX_SYSCALL(__NR_close, fd);
+ }
+}
+
static long int dserver_rpc_hooks_send_message(int socket, const dserver_rpc_hooks_msghdr_t* message) {
+ rclol_rpc_trace_sample(message, (void**) __builtin_frame_address(0));
+ if (slowwait_enabled() && LINUX_SYSCALL(__NR_gettid) == LINUX_SYSCALL(__NR_getpid) &&
+ message != 0 && message->msg_iov != 0 && message->msg_iovlen > 0 && message->msg_iov->iov_base != 0 && message->msg_iov->iov_len >= sizeof(int)) {
+ slowrpc_pending_call = *(const int*) message->msg_iov->iov_base;
+ slowrpc_start = slowwait_now();
+ }
+#if 0 /* darling-lol M58F145: disabled (was a hot per-RPC stdout flood that
+ * slowed the Riot launcher's update into its 30 s no-progress timeout) */
+ if (message != 0 && message->msg_iov != 0 && message->msg_iovlen > 0 &&
+ message->msg_iov->iov_base != 0 && message->msg_iov->iov_len >= sizeof(int)) {
+ static unsigned long m58ar_sn = 0;
+ if (m58ar_sn++ < 1000000) {
+ int callnum = *(const int*) message->msg_iov->iov_base;
+ __simple_printf("M58AR send tid=%d sock=%d call=%d\n",
+ (int) dserver_rpc_hooks_get_tid(), socket, callnum);
+ }
+ }
+#endif
#ifdef __NR_socketcall
return LINUX_SYSCALL(__NR_socketcall, LINUX_SYS_SENDMSG, ((long[6]) { socket, message, 0 }));
#else
@@ -83,19 +144,108 @@ static long int dserver_rpc_hooks_send_message(int socket, const dserver_rpc_hoo
#endif
};
+/* darling-lol: wait for the reply by polling for a short time before sleeping (main thread only).
+ *
+ * Most requests are answered within tens of microseconds, but the caller went to sleep in recvmsg at once, so every
+ * round trip also paid for descheduling the thread, idling the core and waking it again. The game's main thread makes
+ * ~40-50 round trips per frame (mutex and condition-variable calls, run-loop port operations, timers). Polling the
+ * socket for a bounded time catches the quick replies without that sleep; a reply that takes longer (a real blocking
+ * wait) falls back to the normal blocking receive.
+ * Host file /tmp/rclol-rpc-spin-us holds the polling budget in microseconds (0 or no file = off), re-read every 4096
+ * main-thread receives so both settings can be compared inside one match. */
+static int rclol_spin_main_socket = -2; /* -2: main thread not identified yet */
+static unsigned long rclol_spin_ticks = 0; /* TSC ticks to poll for; 0 = off */
+static unsigned long rclol_spin_calls = 0;
+static unsigned long rclol_spin_hits = 0, rclol_spin_misses = 0;
+
+static void rclol_spin_refresh(void) {
+ char digits[16];
+ long value = 0;
+ long fd = LINUX_SYSCALL(__NR_open, "/tmp/rclol-rpc-spin-us", 0, 0);
+ if (fd >= 0) {
+ long n = LINUX_SYSCALL(__NR_read, fd, digits, sizeof(digits) - 1);
+ LINUX_SYSCALL(__NR_close, fd);
+ for (long i = 0; i < n && digits[i] >= '0' && digits[i] <= '9'; ++i) value = value * 10 + (digits[i] - '0');
+ if (value > 500) value = 500;
+ }
+ /* the TSC runs at a fixed rate near the nominal clock; ~2300 ticks per microsecond on this machine. A wrong
+ * guess only changes the budget, not the result. */
+ rclol_spin_ticks = (unsigned long) value * 2300ul;
+ if (value > 0) {
+ /* counters for the A/B: how often the reply arrived while polling */
+ char line[120];
+ int len = __simple_sprintf(line, "spin_us=%ld hits=%lu misses=%lu\n", value, rclol_spin_hits, rclol_spin_misses);
+ long log = LINUX_SYSCALL(__NR_open, "/tmp/rclol-rpc-spin.log", 02101, 0666);
+ if (log >= 0) { LINUX_SYSCALL(__NR_write, log, line, len); LINUX_SYSCALL(__NR_close, log); }
+ }
+}
+
+static inline unsigned long rclol_rdtsc(void) {
+ unsigned int lo, hi;
+ __asm__ volatile("rdtsc" : "=a"(lo), "=d"(hi));
+ return ((unsigned long) hi << 32) | lo;
+}
+
static long int dserver_rpc_hooks_receive_message(int socket, dserver_rpc_hooks_msghdr_t* out_message) {
long int ret = 0;
retry:
+#if defined(__x86_64__) && !defined(__NR_socketcall)
+ if (rclol_spin_main_socket == -2) {
+ if (LINUX_SYSCALL(__NR_gettid) == LINUX_SYSCALL(__NR_getpid)) {
+ rclol_spin_main_socket = socket;
+ rclol_spin_refresh();
+ }
+ }
+ if (socket == rclol_spin_main_socket) {
+ if ((++rclol_spin_calls & 4095) == 0)
+ rclol_spin_refresh();
+ if (rclol_spin_ticks != 0) {
+ const unsigned long deadline = rclol_rdtsc() + rclol_spin_ticks;
+ for (;;) {
+ ret = LINUX_SYSCALL(__NR_recvmsg, socket, out_message, 0x40 /* MSG_DONTWAIT */);
+ if (ret != -11 /* EAGAIN */) {
+ ++rclol_spin_hits;
+ goto received;
+ }
+ if (rclol_rdtsc() >= deadline)
+ break;
+ for (int i = 0; i < 24; ++i) __asm__ volatile("pause");
+ }
+ ++rclol_spin_misses;
+ }
+ }
+#endif
#ifdef __NR_socketcall
ret = LINUX_SYSCALL(__NR_socketcall, LINUX_SYS_RECVMSG, ((long[6]) { socket, out_message, 0 }));
#else
ret = LINUX_SYSCALL(__NR_recvmsg, socket, out_message, 0);
+#endif
+#if defined(__x86_64__) && !defined(__NR_socketcall)
+received:
#endif
if (ret < 0) {
return ret;
}
+#if 0 /* darling-lol M58F145: disabled (hot per-RPC stdout flood; see above) */
+ if (ret >= (long int) sizeof(int) && out_message->msg_iov != 0 &&
+ out_message->msg_iov->iov_base != 0) {
+ static unsigned long m58ar_rn = 0;
+ if (m58ar_rn++ < 1000000) {
+ int m58ar_rcall = *(const int*) out_message->msg_iov->iov_base;
+ __simple_printf("M58AR recv tid=%d sock=%d call=%d\n",
+ (int) dserver_rpc_hooks_get_tid(), socket, m58ar_rcall);
+ }
+ }
+#endif
+
+ if (slowrpc_pending_call >= 0 && ret >= (long int) sizeof(int) && out_message->msg_iov != 0 && out_message->msg_iov->iov_base != 0 &&
+ *(const int*) out_message->msg_iov->iov_base != 0x52cca11 && LINUX_SYSCALL(__NR_gettid) == LINUX_SYSCALL(__NR_getpid)) {
+ slowwait_log("rpc", (unsigned int) slowrpc_pending_call, slowrpc_start, (void**) __builtin_frame_address(0), 0, 0);
+ slowrpc_pending_call = -1;
+ }
+
if (ret >= sizeof(dserver_s2c_callhdr_t)) {
dserver_s2c_callhdr_t* callhdr = out_message->msg_iov->iov_base;
@@ -280,16 +430,21 @@ __attribute__((format(printf, 1, 2)))
void __dserver_rpc_hooks_printf(const char* format, ...);
#define dserver_rpc_hooks_printf __dserver_rpc_hooks_printf
-#define dserver_rpc_hooks_atomic_save_t sigset_t
+#define dserver_rpc_hooks_atomic_save_t linux_sigset_t
static void dserver_rpc_hooks_atomic_begin(dserver_rpc_hooks_atomic_save_t* atomic_save) {
- // block standard unix signals (not real-time signals, though)
- sigset_t set = 0x7fffffff;
- sys_sigprocmask(SIG_BLOCK, &set, atomic_save);
+ /* Block standard signals, leaving real-time S2C signals available. The
+ * kernel ignores SIGKILL/SIGSTOP. Keep the original full native mask:
+ * converting it through a BSD mask loses unsupported real-time bits and
+ * needlessly scans/converts three masks on every RPC round trip. */
+ linux_sigset_t set = 0x7fffffff;
+ LINUX_SYSCALL(__NR_rt_sigprocmask, 0 /* SIG_BLOCK */, &set,
+ atomic_save, sizeof(set));
};
static void dserver_rpc_hooks_atomic_end(dserver_rpc_hooks_atomic_save_t* atomic_save) {
- sys_sigprocmask(SIG_SETMASK, atomic_save, NULL);
+ LINUX_SYSCALL(__NR_rt_sigprocmask, 2 /* SIG_SETMASK */, atomic_save,
+ NULL, sizeof(*atomic_save));
};
#define dserver_rpc_hooks_get_interrupt_status() (-LINUX_EINTR)
diff --git a/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/guarded/change_fdguard_np.h b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/guarded/change_fdguard_np.h
new file mode 100644
index 0000000..ac5a0e6
--- /dev/null
+++ b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/guarded/change_fdguard_np.h
@@ -0,0 +1,8 @@
+#ifndef LINUX_CHANGE_FDGUARD_H
+#define LINUX_CHANGE_FDGUARD_H
+
+#include <stdint.h>
+
+long sys_change_fdguard_np(int fd, uint64_t* guard, uint64_t* nguard, unsigned guardflags, int* fdflagsp);
+
+#endif // LINUX_CHANGE_FDGUARD_H
diff --git a/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/mac_syscall.h b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/mac_syscall.h
new file mode 100644
index 0000000..b4c307a
--- /dev/null
+++ b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/mac_syscall.h
@@ -0,0 +1,6 @@
+#ifndef LINUX_MAC_SYSCALL_H
+#define LINUX_MAC_SYSCALL_H
+
+long sys___mac_syscall(const char* policy, int call, void* arg);
+
+#endif // LINUX_MAC_SYSCALL_H
diff --git a/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/ptrace.h b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/ptrace.h
index 5d4beb5..9d5de46 100644
--- a/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/ptrace.h
+++ b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/ptrace.h
@@ -9,6 +9,7 @@
#define XNU_PT_DETACH 11
#define XNU_PT_SIGEXC 12
#define XNU_PT_THUPDATE 13
+#define XNU_PT_DENY_ATTACH 31
long sys_ptrace(int request, int pid, void* addr, int data);
diff --git a/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/psynch/ulock_wait.h b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/psynch/ulock_wait.h
index 90bc146..5bcea21 100644
--- a/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/psynch/ulock_wait.h
+++ b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/psynch/ulock_wait.h
@@ -5,6 +5,8 @@
long sys_ulock_wait(uint32_t operation, void* addr, uint64_t value, uint32_t timeout);
+long sys_ulock_wait2(uint32_t operation, void* addr, uint64_t value, uint64_t timeout, uint64_t value2);
+
#define XNU_UL_OPCODE_MASK 0xff
#define XNU_UL_COMPARE_AND_WAIT 1
#define XNU_UL_UNFAIR_LOCK 2
diff --git a/darling/src/libsystem_kernel/emulation/src/common/base.c b/darling/src/libsystem_kernel/emulation/src/common/base.c
index d077d04..3dcafea 100644
--- a/darling/src/libsystem_kernel/emulation/src/common/base.c
+++ b/darling/src/libsystem_kernel/emulation/src/common/base.c
@@ -23,6 +23,284 @@ int __linux_syscall(int nr, long a1, long a2, long a3, long a4, long a5, long a6
return linux_syscall(a1, a2, a3, a4, a5, a6, nr);
}
+/* darling-lol M58x: bounded per-thread trace of the host syscalls that can
+ * block. Every guest host-syscall goes through the LINUX_SYSCALL* macros,
+ * which now route here. We print the syscall number, its first argument
+ * (usually the fd) and the host tid for the first few calls of each thread
+ * and then sparsely, so a stalled run names the syscall each parked thread
+ * is in without flooding the log. */
+#define LOLSYS_SLOTS 256
+
+struct lolsys_slot {
+ int tid;
+ unsigned int count;
+};
+
+static struct lolsys_slot lolsys_slots[LOLSYS_SLOTS];
+
+static int lolsys_traced(int nr)
+{
+ switch (nr) {
+#ifdef __NR_read
+ case __NR_read: return 1;
+#endif
+#ifdef __NR_recvmsg
+ case __NR_recvmsg: return 1;
+#endif
+#ifdef __NR_recvfrom
+ case __NR_recvfrom: return 1;
+#endif
+#ifdef __NR_poll
+ case __NR_poll: return 1;
+#endif
+#ifdef __NR_select
+ case __NR_select: return 1;
+#endif
+#ifdef __NR_accept
+ case __NR_accept: return 1;
+#endif
+#ifdef __NR_connect
+ case __NR_connect: return 1;
+#endif
+#ifdef __NR_nanosleep
+ case __NR_nanosleep: return 1;
+#endif
+ /* darling-lol M58F73: widen the trace for the Riot launcher's early
+ * startup. The launcher dies with exit 252 before writing any log,
+ * after open()ing /proc/<pid>/mounts; these calls name the failing step
+ * (filesystem/space probes, sysctl, anti-debug). */
+#ifdef __NR_statfs
+ case __NR_statfs: return 1;
+#endif
+#ifdef __NR_fstatfs
+ case __NR_fstatfs: return 1;
+#endif
+#ifdef __NR_statvfs
+ case __NR_statvfs: return 1;
+#endif
+#ifdef __NR_fstatvfs
+ case __NR_fstatvfs: return 1;
+#endif
+#ifdef __NR_open
+ case __NR_open: return 1;
+#endif
+#ifdef __NR_openat
+ case __NR_openat: return 1;
+#endif
+#ifdef __NR__sysctl
+ case __NR__sysctl: return 1;
+#endif
+#ifdef __NR_sysctl
+ case __NR_sysctl: return 1;
+#endif
+#ifdef __NR_ptrace
+ case __NR_ptrace: return 1;
+#endif
+#ifdef __NR_access
+ case __NR_access: return 1;
+#endif
+#ifdef __NR_newfstatat
+ case __NR_newfstatat: return 1;
+#endif
+#ifdef __NR_fstat
+ case __NR_fstat: return 1;
+#endif
+#ifdef __NR_readlink
+ case __NR_readlink: return 1;
+#endif
+#ifdef __NR_exit
+ case __NR_exit: return 1;
+#endif
+#ifdef __NR_exit_group
+ case __NR_exit_group: return 1;
+#endif
+ default:
+ return 0;
+ }
+}
+
+/* darling-lol M58F76: for the path-bearing syscalls, return the pointer to the
+ * guest C string so the LOLSC line names the file/directory the launcher
+ * touches right before it exits. */
+static const char* __attribute__((unused)) lolsys_path(int nr, long a1, long a2)
+{
+ switch (nr) {
+#ifdef __NR_open
+ case __NR_open: return (const char*) a1;
+#endif
+#ifdef __NR_openat
+ case __NR_openat: return (const char*) a2; /* (dirfd, path, flags, mode) */
+#endif
+#ifdef __NR_readlink
+ case __NR_readlink: return (const char*) a1;
+#endif
+#ifdef __NR_statfs
+ case __NR_statfs: return (const char*) a1;
+#endif
+#ifdef __NR_access
+ case __NR_access: return (const char*) a1;
+#endif
+#ifdef __NR_newfstatat
+ case __NR_newfstatat: return (const char*) a2; /* (dirfd, path, stat, flags) */
+#endif
+ default: return 0;
+ }
+}
+
+/* darling-lol M58F121: microsecond timestamp (gettimeofday) shared with the
+ * dserver M58RD HANDOFF/QUEUED log clock, for wake-to-run correlation. */
+static long __attribute__((unused)) lolsrv_us(void)
+{
+ struct { long tv_sec; long tv_usec; } tv;
+ tv.tv_sec = 0;
+ tv.tv_usec = 0;
+ linux_syscall((long) &tv, 0, 0, 0, 0, 0, __NR_gettimeofday);
+ return tv.tv_sec * 1000000L + tv.tv_usec;
+}
+
+/* darling-lol M58F159: opt-in long-wait trace flag. Enabled from outside (the
+ * injected shim calls __lolsys_set_wake_trace(1)); the syscall wrapper itself
+ * must not call libc/getenv (M58F158: that wedged the container). */
+static int lolsys_wake_on = 0;
+
+VISIBLE
+void __lolsys_set_wake_trace(int on)
+{
+ lolsys_wake_on = on;
+}
+
+VISIBLE
+long __lolsys_syscall(long a1, long a2, long a3, long a4, long a5, long a6, int nr)
+{
+ /* Disabled diagnostics must not add another host syscall or update shared
+ * counters on every receive/wait. Keep the opt-in trace path below. */
+ if (!lolsys_wake_on)
+ return linux_syscall(a1, a2, a3, a4, a5, a6, nr);
+ long ret = 0;
+ /* darling-lol M58F166: flag-gated recvmsg rate/errno counter (no libc), to
+ * confirm the busy recvmsg loop independently of the SIGPROF profiler. */
+#ifdef __NR_recvmsg
+ if (lolsys_wake_on && nr == __NR_recvmsg) {
+ static unsigned long rn = 0, rok = 0, reintr = 0, reagain = 0, rother = 0, rlines = 0;
+ long rr = linux_syscall(a1, a2, a3, a4, a5, a6, nr);
+ rn++;
+ if (rr >= 0) rok++;
+ else if (rr == -4) reintr++;
+ else if (rr == -11) reagain++;
+ else rother++;
+ if ((rn & 4095) == 0 && rlines < 500) {
+ int rpid = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_getpid);
+ int rtid = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_gettid);
+ rlines++;
+ __simple_printf("LOLRCV pid=%d tid=%d fd=%ld n=%lu ok=%lu eintr=%lu eagain=%lu other=%lu\n",
+ rpid, rtid, a1, rn, rok, reintr, reagain, rother);
+ }
+ return rr;
+ }
+#endif
+ /* darling-lol M58F155: opt-in long-wait trace (RCLOL_WAKE=1). For the
+ * blocking waits that make up the browser main thread's runloop, log only
+ * waits longer than ~200 ms (low volume), so an intermittent multi-second
+ * loop stall names itself. Capped. */
+#ifdef __NR_recvmsg
+ if (lolsys_wake_on && (nr == __NR_recvmsg
+#ifdef __NR_poll
+ || nr == __NR_poll
+#endif
+#ifdef __NR_epoll_wait
+ || nr == __NR_epoll_wait
+#endif
+ )) {
+ static unsigned long lw_cap = 0;
+ int lw_tid = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_gettid);
+ long lw_t0 = lolsrv_us();
+ long lw_ret = linux_syscall(a1, a2, a3, a4, a5, a6, nr);
+ long lw_dt = lolsrv_us() - lw_t0;
+ if (lw_dt > 200000 && lw_cap++ < 5000) {
+ int lw_pid = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_getpid);
+ __simple_printf("LOLWAKE pid=%d tid=%d nr=%d fd=%ld dt_ms=%ld ret=%ld\n",
+ lw_pid, lw_tid, nr, a1, lw_dt / 1000, lw_ret);
+ }
+ return lw_ret;
+ }
+#endif
+ /* M58F121: timestamp slot-port mach receives (fd >= 524000) to measure the
+ * guest-side wake-to-run; correlate with dserver M58RD HANDOFF on the same
+ * port. Bounded per thread. */
+#if 0 /* darling-lol M58F145: disabled (hot mach-recv stdout flood; see M58F140/141) */
+#ifdef __NR_recvmsg
+ if (nr == __NR_recvmsg && a1 >= 524000) {
+ int tid2 = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_gettid);
+ struct lolsys_slot* s2 = &lolsys_slots[((unsigned int) tid2) & (LOLSYS_SLOTS - 1)];
+ if (s2->tid != tid2) {
+ s2->tid = tid2;
+ s2->count = 0;
+ }
+ ret = linux_syscall(a1, a2, a3, a4, a5, a6, nr);
+ if (s2->count < 512) {
+ int pid2 = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_getpid);
+ long t = lolsrv_us();
+ __simple_printf("LOLSRV pid=%d tid=%d fd=%ld sec=%ld usec=%ld ret=%ld\n",
+ pid2, tid2, a1, t / 1000000L, t % 1000000L, ret);
+ }
+ s2->count++;
+ return ret;
+ }
+#endif
+#endif
+ if (lolsys_traced(nr)) {
+ int tid = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_gettid);
+ struct lolsys_slot* s = &lolsys_slots[((unsigned int) tid) & (LOLSYS_SLOTS - 1)];
+ if (s->tid != tid) {
+ s->tid = tid;
+ s->count = 0;
+ }
+ int log_it = (s->count < 3) || ((s->count & 255) == 0);
+ /* M58F73: the "naming" calls (fs probes / open / sysctl / ptrace) are
+ * logged every time so the launcher's exact early sequence is visible;
+ * exit/exit_group are logged so the exact exit code is captured. */
+ switch (nr) {
+#ifdef __NR_statfs
+ case __NR_statfs: log_it = 1; break;
+#endif
+#ifdef __NR_fstatfs
+ case __NR_fstatfs: log_it = 1; break;
+#endif
+#ifdef __NR_open
+ case __NR_open: log_it = 1; break;
+#endif
+#ifdef __NR_openat
+ case __NR_openat: log_it = 1; break;
+#endif
+#ifdef __NR__sysctl
+ case __NR__sysctl: log_it = 1; break;
+#endif
+#ifdef __NR_ptrace
+ case __NR_ptrace: log_it = 1; break;
+#endif
+#ifdef __NR_newfstatat
+ case __NR_newfstatat: log_it = 1; break;
+#endif
+#ifdef __NR_exit
+ case __NR_exit: log_it = 1; break;
+#endif
+#ifdef __NR_exit_group
+ case __NR_exit_group: log_it = 1; break;
+#endif
+ default: break;
+ }
+ if (log_it) {
+ /* darling-lol M58F145: LOLSC stdout logging disabled (hot; it
+ * flooded the launcher's stdout). Keep the syscall + counter. */
+ ret = linux_syscall(a1, a2, a3, a4, a5, a6, nr);
+ s->count++;
+ return ret;
+ }
+ s->count++;
+ }
+ return linux_syscall(a1, a2, a3, a4, a5, a6, nr);
+}
+
#ifdef __TESTING
void _start()
{
diff --git a/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/clock_gettime.c b/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/clock_gettime.c
index f27a888..1bbfc73 100644
--- a/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/clock_gettime.c
+++ b/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/clock_gettime.c
@@ -3,13 +3,334 @@
#include <darling/emulation/common/base.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
+#include <elfcalls.h>
+
extern long cerror(int __err);
+extern struct elf_calls* _elfcalls;
+
+#if defined(__x86_64__)
+/* darling-lol: read the clock through the host vDSO.
+ *
+ * The guest process is a native Linux process, so the kernel's vDSO is mapped
+ * and its __vdso_clock_gettime can be called directly - the same ELF bridge
+ * libmalloc uses for __vdso_getcpu. A vDSO read uses the same clocksource as
+ * the syscall, so no calibration and no /tmp opt-in are involved.
+ *
+ * The pointer is resolved once per process. Resolution runs dlopen/dlsym
+ * through the native loader, so it is also done eagerly from mach_driver_init()
+ * where the ELF bridge is known to be usable; the steady-state path is one
+ * atomic load and an indirect call, which keeps clock reads from signal
+ * handlers and RPC-guarded callers safe. A missing bridge is not cached: the
+ * next call retries. */
+static void* vdso_clock_gettime_fn; /* 0 unresolved, -1 absent, else the vDSO entry */
+
+/* Some glibc versions expose the __vdso_* entries only through the LINUX_2.6
+ * symbol version (libmalloc's __vdso_getcpu lookup uses dlvsym for the same
+ * reason). */
+static void* vdso_symbol(struct elf_calls* calls, void* vdso, const char* name, const char* version)
+{
+ void* function = calls->dlsym(vdso, name);
+ if (function == 0 && version != 0)
+ {
+ void* libc = calls->dlopen("libc.so.6");
+ void* (*dlvsym)(void*, const char*, const char*) = libc ? calls->dlsym(libc, "dlvsym") : 0;
+ if (dlvsym != 0)
+ function = dlvsym(vdso, name, version);
+ }
+ return function;
+}
+
+static void* vdso_clock_gettime_resolve(void)
+{
+ void* function = __atomic_load_n(&vdso_clock_gettime_fn, __ATOMIC_ACQUIRE);
+ struct elf_calls* calls;
+
+ if (function != 0)
+ return function == (void*) -1 ? 0 : function;
+
+ calls = _elfcalls;
+ if (calls == 0 || calls->dlopen == 0 || calls->dlsym == 0)
+ return 0; /* the bridge is not up yet; retry on the next call */
+
+ if (!__atomic_compare_exchange_n(&vdso_clock_gettime_fn, &function, (void*) -1, 0,
+ __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE))
+ return function == (void*) -1 ? 0 : function;
+
+ {
+ void* vdso = calls->dlopen("linux-vdso.so.1");
+ void* resolved = vdso ? vdso_symbol(calls, vdso, "__vdso_clock_gettime", "LINUX_2.6") : 0;
+ __atomic_store_n(&vdso_clock_gettime_fn, resolved ? resolved : (void*) -1, __ATOMIC_RELEASE);
+ return resolved;
+ }
+}
+
+/* The guest syscall trampoline does not promise ELF stack alignment (sys_fork
+ * realigns for the same reason) and the vDSO is System V ABI code: realign the
+ * stack for the call. noinline keeps the realignment when this is inlined. */
+__attribute__((noinline))
+__attribute__((force_align_arg_pointer))
+static int vdso_clock_gettime_call(void* function, int clockid, struct timespec* res)
+{
+ return ((int (*)(int, struct timespec*)) function)(clockid, res);
+}
+
+/* 0: *res filled by the vDSO; 1: no vDSO answer, the caller must fall back */
+__attribute__((visibility("hidden")))
+int __darling_vdso_clock_gettime(int clockid, struct timespec* res)
+{
+ void* function = vdso_clock_gettime_resolve();
+
+ if (function == 0)
+ return 1;
+ if (vdso_clock_gettime_call(function, clockid, res) != 0)
+ return 1;
+
+ return 0;
+}
+
+/* Resolve before the first clock read can happen in a signal handler or with
+ * the RPC signal mask held. Called from mach_driver_init(). */
+__attribute__((visibility("hidden")))
+void __darling_vdso_clock_init(void)
+{
+ vdso_clock_gettime_resolve();
+}
+#endif /* __x86_64__ */
+
+#if defined(__x86_64__)
+/* darling-lol: user-space CLOCK_MONOTONIC / CLOCK_REALTIME fallback.
+ *
+ * The host vDSO is tried first (see above); this path is the fallback for a host whose vDSO cannot serve
+ * the clock (for example an HPET kernel clocksource where the vDSO falls back to the syscall as well and
+ * one read costs ~1.4 us), and the League main thread makes ~200 clock reads per frame.
+ *
+ * Each core's TSC still counts at one constant rate; only the offsets between cores differ. RDTSCP returns the TSC and
+ * the CPU it was read on in one instruction, so the clock is kept per CPU: a (TSC, monotonic ns) pair sampled with a
+ * real syscall, extrapolated for at most FC_WINDOW_NS and then sampled again. The tick rate is measured between such
+ * samples of one CPU. Results never go backwards (the sampling error between two cores is about a microsecond).
+ * OPT-IN: only with the host file /tmp/rclol-fast-clock (read once per process), on CPUs with an invariant TSC, for
+ * these two clock ids. Measured 2026-10-10 in League with it on: darlingserver 113-120% CPU with continuous
+ * mach_vm_region_recurse traffic against 38-57% with it off, and slower fights. With a TSC kernel clocksource a real
+ * read is ~0.35 us, so little is lost by leaving it off. */
+#define FC_CPUS 256
+#define FC_WINDOW_NS 100000000ull
+#define FC_FIRST_SPAN_NS 20000000ull
+#define FC_GOOD_SPAN_NS 500000000ull
+#define FC_MAX_SPAN_NS 2000000000ull
+#define FC_MAX_SAMPLE_TICKS 60000ull
+#define FC_SAMPLE_SLACK_TICKS 400ull
+
+struct fc_cpu {
+ volatile unsigned int seq; /* odd while a writer is inside */
+ volatile unsigned int valid;
+ volatile unsigned long long tsc;
+ volatile unsigned long long mono;
+ volatile long long real_offset;
+ unsigned long long origin_tsc;
+ unsigned long long origin_mono;
+} __attribute__((aligned(64)));
+
+static struct fc_cpu fc_cpus[FC_CPUS];
+static volatile unsigned long long fc_mult; /* ns per tick, 32.32 fixed point; 0 until measured */
+static volatile unsigned long long fc_window; /* FC_WINDOW_NS in ticks */
+static volatile unsigned long long fc_last_mono __attribute__((aligned(64)));
+static volatile unsigned long long fc_fastest = FC_MAX_SAMPLE_TICKS; /* shortest bracketed clock read seen, in ticks */
+static volatile int fc_state; /* 0 not probed, 1 usable, -1 unusable */
+
+static inline unsigned long long fc_rdtscp(unsigned int* cpu)
+{
+ unsigned int lo, hi, aux;
+ __asm__ volatile("rdtscp" : "=a"(lo), "=d"(hi), "=c"(aux));
+ *cpu = aux & 0xfff;
+ return ((unsigned long long) hi << 32) | lo;
+}
+
+static int fc_probe(void)
+{
+ unsigned int a, b, c, d;
+ int usable = 0;
+ __asm__ volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(0x80000000u), "c"(0));
+ if (a >= 0x80000007u) {
+ __asm__ volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(0x80000001u), "c"(0));
+ usable = (d >> 27) & 1; /* RDTSCP */
+ __asm__ volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(0x80000007u), "c"(0));
+ usable &= (d >> 8) & 1; /* invariant TSC */
+ }
+ if (usable && LINUX_SYSCALL(__NR_access, "/tmp/rclol-fast-clock", 0) != 0)
+ usable = 0;
+ fc_state = usable ? 1 : -1;
+ return fc_state;
+}
+
+static inline unsigned long long fc_monotonic(unsigned long long ns)
+{
+ unsigned long long last = __atomic_load_n(&fc_last_mono, __ATOMIC_RELAXED);
+ while (ns > last) {
+ if (__atomic_compare_exchange_n(&fc_last_mono, &last, ns, 1, __ATOMIC_RELAXED, __ATOMIC_RELAXED))
+ return ns;
+ }
+ return last;
+}
+
+static inline void fc_store(int clockid, struct timespec* res, unsigned long long mono, long long real_offset)
+{
+ unsigned long long ns = fc_monotonic(mono);
+ if (clockid == CLOCK_REALTIME)
+ ns += real_offset;
+ res->tv_sec = ns / 1000000000ull;
+ res->tv_nsec = ns % 1000000000ull;
+}
+
+static int fc_resync(int clockid, struct timespec* res)
+{
+ struct timespec mono, real;
+ unsigned int c0, c1, c2;
+ unsigned long long t0, t1, t2, mono_ns, real_ns, mult, fastest, limit;
+ int rv;
+
+ t0 = fc_rdtscp(&c0);
+ rv = LINUX_SYSCALL(__NR_clock_gettime, CLOCK_MONOTONIC, &mono);
+ if (rv < 0)
+ return rv;
+ t1 = fc_rdtscp(&c1);
+ mono_ns = (unsigned long long) mono.tv_sec * 1000000000ull + mono.tv_nsec;
+
+ /* A sample is only a calibration point when the read ran undisturbed on one CPU: not much slower than the fastest
+ * read seen. The reference creeps up on every rejection, so a machine that became slower (clock throttled) is
+ * followed within a few calls, and a rejected call costs the one syscall it would have cost anyway. */
+ fastest = fc_fastest;
+ if (c0 == c1 && t1 - t0 < fastest)
+ fc_fastest = fastest = t1 - t0;
+ limit = fastest + fastest / 2 + FC_SAMPLE_SLACK_TICKS;
+ if (c0 != c1 || c0 >= FC_CPUS || t1 - t0 >= limit) {
+ if (c0 == c1 && fastest < FC_MAX_SAMPLE_TICKS)
+ fc_fastest = fastest + fastest / 8 + 1;
+ if (clockid == CLOCK_REALTIME) {
+ rv = LINUX_SYSCALL(__NR_clock_gettime, CLOCK_REALTIME, res);
+ return rv < 0 ? rv : 0;
+ }
+ goto done;
+ }
+
+ rv = LINUX_SYSCALL(__NR_clock_gettime, CLOCK_REALTIME, &real);
+ if (rv < 0)
+ return rv;
+ t2 = fc_rdtscp(&c2);
+ real_ns = (unsigned long long) real.tv_sec * 1000000000ull + real.tv_nsec;
+ mult = fc_mult;
+
+ if (c1 == c2 && t2 - t1 < limit) {
+ struct fc_cpu* c = &fc_cpus[c0];
+ unsigned int seq = __atomic_load_n(&c->seq, __ATOMIC_RELAXED);
+ if (!(seq & 1) && __atomic_compare_exchange_n(&c->seq, &seq, seq + 1, 0, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) {
+ /* The kernel reads the clock early in the syscall and most of the round trip is the return path. */
+ unsigned long long mid = t0 + (t1 - t0) / 4;
+ unsigned long long real_mid = t1 + (t2 - t1) / 4;
+ int restart = !c->valid || mid <= c->origin_tsc || mono_ns <= c->origin_mono;
+ if (!restart) {
+ unsigned long long span = mono_ns - c->origin_mono;
+ unsigned long long ticks = mid - c->origin_tsc;
+ if (span > 2 * FC_MAX_SPAN_NS) {
+ restart = 1; /* long idle or suspend: too long for the fixed-point division */
+ } else if (span >= FC_FIRST_SPAN_NS) {
+ unsigned long long measured = (span << 32) / ticks;
+ /* plausible tick rates only: 0.4 .. 16 GHz */
+ int plausible = measured > (1ull << 28) && measured < (5ull << 31);
+ if (plausible && mult) {
+ unsigned long long diff = measured > mult ? measured - mult : mult - measured;
+ if (diff > mult / 128)
+ plausible = 0; /* the TSC and the kernel clock diverged (suspend): start over */
+ }
+ if (!plausible) {
+ restart = 1;
+ } else if (!mult || span >= FC_GOOD_SPAN_NS) {
+ mult = measured;
+ __atomic_store_n(&fc_window, (FC_WINDOW_NS << 32) / measured, __ATOMIC_RELAXED);
+ __atomic_store_n(&fc_mult, measured, __ATOMIC_RELEASE);
+ }
+ if (span >= FC_MAX_SPAN_NS)
+ restart = 1;
+ }
+ }
+ if (restart) {
+ c->origin_tsc = mid;
+ c->origin_mono = mono_ns;
+ }
+ c->tsc = mid;
+ c->mono = mono_ns;
+ c->real_offset = (long long) (real_ns - mono_ns) - (long long) (((real_mid - mid) * mult) >> 32);
+ c->valid = 1;
+ __atomic_store_n(&c->seq, seq + 2, __ATOMIC_RELEASE);
+ }
+ }
+
+ if (clockid == CLOCK_REALTIME) {
+ *res = real;
+ return 0;
+ }
+done:
+ mono_ns = fc_monotonic(mono_ns);
+ res->tv_sec = mono_ns / 1000000000ull;
+ res->tv_nsec = mono_ns % 1000000000ull;
+ return 0;
+}
+
+/* 0: *res filled; 1: not handled here (caller makes the syscall); negative: Linux errno */
+__attribute__((visibility("hidden")))
+int __darling_fast_clock(int clockid, struct timespec* res)
+{
+ unsigned int cpu, seq;
+ unsigned long long tsc, mult;
+ int state = fc_state;
+
+ if (__builtin_expect(state <= 0, 0)) {
+ if (state == 0)
+ state = fc_probe();
+ if (state < 0)
+ return 1;
+ }
+ if (clockid != CLOCK_MONOTONIC && clockid != CLOCK_REALTIME)
+ return 1;
+
+ mult = __atomic_load_n(&fc_mult, __ATOMIC_ACQUIRE);
+ tsc = fc_rdtscp(&cpu);
+ if (__builtin_expect(mult != 0 && cpu < FC_CPUS, 1)) {
+ struct fc_cpu* c = &fc_cpus[cpu];
+ seq = __atomic_load_n(&c->seq, __ATOMIC_ACQUIRE);
+ if (!(seq & 1) && c->valid) {
+ unsigned long long base_tsc = c->tsc, base_mono = c->mono;
+ long long real_offset = c->real_offset;
+ if (__atomic_load_n(&c->seq, __ATOMIC_ACQUIRE) == seq) {
+ unsigned long long delta = tsc - base_tsc;
+ if (delta < fc_window) {
+ fc_store(clockid, res, base_mono + ((delta * mult) >> 32), real_offset);
+ return 0;
+ }
+ }
+ }
+ }
+ return fc_resync(clockid, res);
+}
+#endif
+
VISIBLE
int clock_gettime (int __clockid, struct timespec* __res)
{
int rv;
+#if defined(__x86_64__)
+ /* Exact and cheaper than the syscall; the vDSO answer comes from the same
+ * clocksource the syscall would read. */
+ rv = __darling_vdso_clock_gettime(__clockid, __res);
+ if (rv == 0)
+ return 0;
+
+ rv = __darling_fast_clock(__clockid, __res);
+ if (rv == 0)
+ return 0;
+ if (rv == 1)
+#endif
rv = LINUX_SYSCALL(__NR_clock_gettime, __clockid, __res);
if (rv < 0)
{
diff --git a/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/file_handle.c b/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/file_handle.c
index 08c05fd..b51d66d 100644
--- a/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/file_handle.c
+++ b/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/file_handle.c
@@ -1,7 +1,7 @@
#include <darling/emulation/linux_premigration/ext/file_handle.h>
#include <sys/errno.h>
-#include <os/lock.h>
+#include <darling/emulation/common/private_mutex.h>
#include <darling/emulation/conversion/errno.h>
#include <darling/emulation/conversion/fcntl/open.h>
@@ -27,10 +27,8 @@ struct SavedRef
#define MOUNT_ID_SAVED (-100)
static struct SavedRef g_savedRefs[200];
static int g_nextSavedRef = 0, g_nextGen = 0;
-static os_unfair_lock g_savedRefLock = OS_UNFAIR_LOCK_INIT;
+static emulation_mutex g_savedRefLock = EMULATION_MUTEX_INITIALIZER;
-void __attribute__((weak)) os_unfair_lock_unlock(os_unfair_lock_t lock) {}
-void __attribute__((weak)) os_unfair_lock_lock(os_unfair_lock_t lock) {}
VISIBLE
int sys_name_to_handle(const char* name, RefData* ref, int follow)
@@ -53,7 +51,7 @@ int sys_name_to_handle(const char* name, RefData* ref, int follow)
// This is unfortunately the case for overlayfs, which doesn't support "nfs_export" along with "metacopy"
if (ret == -LINUX_EOPNOTSUPP && sys_access(name, 0) == 0)
{
- os_unfair_lock_lock(&g_savedRefLock);
+ emulation_mutex_lock(&g_savedRefLock);
if (g_savedRefs[g_nextSavedRef].path)
free(g_savedRefs[g_nextSavedRef].path);
@@ -66,7 +64,7 @@ int sys_name_to_handle(const char* name, RefData* ref, int follow)
g_nextSavedRef = (g_nextSavedRef + 1) % (sizeof(g_savedRefs) / sizeof(g_savedRefs[0]));
- os_unfair_lock_unlock(&g_savedRefLock);
+ emulation_mutex_unlock(&g_savedRefLock);
ret = 0;
}
@@ -83,7 +81,7 @@ int sys_handle_to_name(RefData* ref, char name[4096])
if (ref->mount_id == MOUNT_ID_SAVED)
{
int ret = -ENOENT;
- os_unfair_lock_lock(&g_savedRefLock);
+ emulation_mutex_lock(&g_savedRefLock);
if (g_savedRefs[ref->index].gen == ref->gen)
{
@@ -91,7 +89,7 @@ int sys_handle_to_name(RefData* ref, char name[4096])
ret = sys_access(name, 0);
}
- os_unfair_lock_unlock(&g_savedRefLock);
+ emulation_mutex_unlock(&g_savedRefLock);
return ret;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/sched_yield.c b/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/sched_yield.c
index dc8fbb8..171746a 100644
--- a/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/sched_yield.c
+++ b/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/sched_yield.c
@@ -1,7 +1,58 @@
#include <darling/emulation/common/base.h>
+#include <darling/emulation/common/simple.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
-void __linux_sched_yield(void)
+/* darling-lol M634: swtch/yield spin detector. The League launcher's
+ * dependency-resolution path pinned a core in a tight yield loop; per-thread
+ * burst counters with caller tags, return addresses and a bounded stack
+ * window identify the guest-side spinner without libc. */
+
+struct lolsys_yield_slot {
+ long tid;
+ unsigned long long n;
+ unsigned int marks;
+};
+
+/* darling-lol: no libc; slot table mirrors base.c's lolsys_slots pattern. */
+#define YIELD_SLOTS 64
+static struct lolsys_yield_slot yield_slots[YIELD_SLOTS];
+
+static int linux_gettid_raw(void)
{
+ return (int) LINUX_SYSCALL0(__NR_gettid);
+}
+
+static void yield_mark(const char* site, unsigned long long n)
+{
+ __simple_printf("LOLYLD site=%s tid=%d n=%llu\n", site, linux_gettid_raw(), n);
+
+}
+
+void __linux_sched_yield_site(const char* site)
+{
+ int tid = linux_gettid_raw();
+ struct lolsys_yield_slot* s = &yield_slots[((unsigned int) tid) % YIELD_SLOTS];
+ if (s->tid != tid) {
+ s->tid = tid;
+ s->n = 0;
+ s->marks = 0;
+ }
+ s->n++;
+ /* Thresholds: ~0.1s, ~1s, ~17s, ~4.5min at 15k/s. Cap: 5 dumps/thread. */
+ static const unsigned long long marks[5] = {1000, 16000, 262144, 4000000, 64000000};
+ if (s->marks < 5) {
+ for (int i = s->marks; i < 5; i++) {
+ if (s->n == marks[i]) {
+ s->marks++;
+ yield_mark(site, s->n);
+ break;
+ }
+ }
+ }
LINUX_SYSCALL0(__NR_sched_yield);
}
+
+void __linux_sched_yield(void)
+{
+ __linux_sched_yield_site("SWTYLD");
+}
diff --git a/darling/src/libsystem_kernel/emulation/src/linux_premigration/signal/sigexc.c b/darling/src/libsystem_kernel/emulation/src/linux_premigration/signal/sigexc.c
index 206e92e..1a763fe 100644
--- a/darling/src/libsystem_kernel/emulation/src/linux_premigration/signal/sigexc.c
+++ b/darling/src/libsystem_kernel/emulation/src/linux_premigration/signal/sigexc.c
@@ -151,8 +151,13 @@ void sigrt_handler(int signum, struct linux_siginfo* info, struct linux_ucontext
int status = dserver_rpc_interrupt_enter();
if (status != 0) {
- __simple_printf("*** dserver_rpc_interrupt_enter failed with code %d ***\n", status);
- __simple_abort();
+ /* darling-lol: same rationale as sigexc_handler -- do not abort
+ * inside the handler when darlingserver is unreachable. RT
+ * signals are control traffic; if we cannot service them, we
+ * simply return (the signal is consumed) rather than kill the
+ * process. */
+ __simple_printf("*** sigrt_handler: dserver_rpc_interrupt_enter failed with code %d (returning) ***\n", status);
+ return;
}
if (signum == SIGNAL_SIGEXC_SUSPEND) {
@@ -267,13 +272,165 @@ static void state_from_kernel(struct linux_ucontext* ctxt, const void* tstate, c
#endif
}
+/* Avoid recursive fault reporting; normal signal handling still runs. */
+static int _lol_depth = 0;
+
+#ifdef __x86_64__
+/* The crashing process's stdout is often not a log its owner can read (the League game's is not),
+ * so the same report is also appended to the host's /tmp/lolsig.log with the module map needed to
+ * resolve the addresses. Only return addresses and the map are written, never data. */
+static void _lol_report_line(int fd, const char* text)
+{
+ if (fd >= 0) {
+ unsigned long length = 0;
+ while (text[length]) ++length;
+ linux_syscall(fd, text, length, 0, 0, 0, __NR_write);
+ }
+}
+
+static void _lol_report_maps(int fd)
+{
+ if (fd < 0) return;
+ int maps = (int) linux_syscall("/proc/self/maps", 0 /* O_RDONLY */, 0, 0, 0, 0, __NR_open);
+ if (maps < 0) return;
+ char chunk[2048];
+ long got;
+ while ((got = linux_syscall(maps, chunk, sizeof(chunk), 0, 0, 0, __NR_read)) > 0)
+ linux_syscall(fd, chunk, got, 0, 0, 0, __NR_write);
+ linux_syscall(maps, 0, 0, 0, 0, 0, __NR_close);
+}
+
+static void _lol_log_fault(int linux_signum, struct linux_siginfo* info, struct linux_ucontext* ctxt)
+{
+ char line[160];
+ /* O_WRONLY | O_CREAT | O_APPEND */
+ int report = (int) linux_syscall("/tmp/lolsig.log", 0x441, 0644, 0, 0, 0, __NR_open);
+ long self = linux_syscall(0, 0, 0, 0, 0, 0, __NR_getpid);
+
+ /* The interrupted stack and argument pointers may be invalid. Reading them
+ * here can cause another signal and replace a recoverable fault with a
+ * fatal crash in the reporter. Keep only code/fault addresses, never data. */
+ __simple_printf("LOLSIGFAIL pid=%ld sig=%d code=%d addr=0x%llx rip=0x%llx\n",
+ self, linux_signum, info->si_code,
+ (unsigned long long) info->si_addr,
+ (unsigned long long) ctxt->uc_mcontext.gregs.rip);
+ __simple_sprintf(line, "LOLSIGFAIL pid=%ld sig=%d code=%d addr=0x%llx rip=0x%llx\n",
+ self, linux_signum, info->si_code,
+ (unsigned long long) info->si_addr,
+ (unsigned long long) ctxt->uc_mcontext.gregs.rip);
+ _lol_report_line(report, line);
+
+ /* Kernel-mediated copies fail safely for inaccessible stacks. Print only
+ * return addresses, never stack contents, strings, or argument values. */
+ struct lol_iovec { void *base; unsigned long size; };
+ unsigned long long frame = ctxt->uc_mcontext.gregs.rbp;
+ for (unsigned i = 0; i < 40 && frame; ++i) {
+ unsigned long long pair[2];
+ struct lol_iovec local = { pair, sizeof(pair) };
+ struct lol_iovec remote = { (void *)frame, sizeof(pair) };
+ long copied = linux_syscall(self, &local, 1, &remote, 1, 0, __NR_process_vm_readv);
+ if (copied != sizeof(pair)) break;
+ __simple_printf("LOLSIGFRAME pid=%ld n=%u pc=0x%llx\n", self, i, pair[1]);
+ __simple_sprintf(line, "LOLSIGFRAME pid=%ld n=%u pc=0x%llx\n", self, i, pair[1]);
+ _lol_report_line(report, line);
+ if (pair[0] <= frame || pair[0] - frame > 1024 * 1024) break;
+ frame = pair[0];
+ }
+
+ _lol_report_line(report, "LOLMAPS\n");
+ _lol_report_maps(report);
+ _lol_report_line(report, "LOLEND\n");
+ if (report >= 0)
+ linux_syscall(report, 0, 0, 0, 0, 0, __NR_close);
+}
+
+#endif
+
+/* Native driver threads have no Darling RPC connection. Forward resize
+ * notifications to the guest main thread without allocating or using RPC.
+ * Other signals, including synchronous faults and queued payloads, retain
+ * their existing handling. Never swallow a failed forward. */
+static int forward_native_resize_signal(int linux_signum, int status) {
+ if (status != -32 /* -LINUX_EPIPE */ || linux_signum != LINUX_SIGWINCH)
+ return 0;
+ long pid = LINUX_SYSCALL(__NR_getpid);
+ if (LINUX_SYSCALL(__NR_gettid) == pid)
+ return 0;
+ return LINUX_SYSCALL(__NR_tgkill, pid, pid, linux_signum) == 0;
+}
+
void sigexc_handler(int linux_signum, struct linux_siginfo* info, struct linux_ucontext* ctxt)
{
int status = dserver_rpc_interrupt_enter();
+ if (forward_native_resize_signal(linux_signum, status))
+ return;
+ int relaxed = 0;
+
+#ifdef __x86_64__
+ if (_lol_depth > 0) {
+ /* Nested fault inside the reporter above: do not recurse, fall
+ * through to normal handling with no further logging. */
+ goto normal;
+ }
+#endif
if (status != 0) {
+ /* darling-lol: previously this hard-aborted, which killed the
+ * process inside the signal handler and hid the original fault
+ * (e.g. a freshly-forked/spawned thread whose per-thread RPC
+ * socket is stale returns -111/ECONNREFUSED here). We cannot
+ * talk to darlingserver, but we can still do the one thing that
+ * matters most: report WHERE the fault happened, then apply the
+ * default signal effects so the process terminates for the
+ * right reason instead of via our abort(). */
__simple_printf("*** dserver_rpc_interrupt_enter failed with code %d ***\n", status);
- __simple_abort();
+#ifdef __x86_64__
+ _lol_depth = 1;
+ _lol_log_fault(linux_signum, info, ctxt);
+ _lol_depth = 0;
+#endif
+ relaxed = 1;
+ }
+
+#ifdef __x86_64__
+ /* darling-lol M38: also report fatal signals that DO reach
+ * darlingserver (the fail-fast branch above only fires when the RPC
+ * socket is dead). Log-and-continue: delivery semantics unchanged. */
+ /* Skip when relaxed: the fail-fast branch above already logged. */
+ if (!relaxed &&
+ (linux_signum == LINUX_SIGABRT || linux_signum == LINUX_SIGSEGV ||
+ linux_signum == LINUX_SIGILL || linux_signum == LINUX_SIGBUS ||
+ linux_signum == LINUX_SIGTRAP)) {
+ _lol_depth = 1;
+ _lol_log_fault(linux_signum, info, ctxt);
+ _lol_depth = 0;
+ }
+normal:
+ /* Self-heal: if a stack read above faulted, its nested entry jumped
+ * here with _lol_depth stuck at 1; reset so later faults still log. */
+ _lol_depth = 0;
+#endif
+
+ if (relaxed) {
+ /* Cannot reach darlingserver: reset the handler to SIG_DFL and
+ * re-raise so the kernel applies the normal signal disposition
+ * (and any core dump). Returning would loop forever on the same
+ * faulting instruction. */
+ struct linux_sigaction sa;
+ sa.sa_sigaction = (linux_sig_handler*) NULL; /* SIG_DFL */
+ sa.sa_mask = 0x7fffffff;
+ sa.sa_flags = LINUX_SA_RESTORER | LINUX_SA_SIGINFO | LINUX_SA_RESTART | LINUX_SA_ONSTACK;
+ sa.sa_restorer = sig_restorer;
+ LINUX_SYSCALL(__NR_rt_sigaction, linux_signum, &sa, NULL, sizeof(sa.sa_mask));
+ /* darling-lol: retarget to the faulting THREAD only. kill(0, sig)
+ * delivers to the whole Linux process group, so one dying guest
+ * process took down every sibling (launcher + UX + daemons all
+ * showed simultaneous SIGABRT/SIGILL). tgkill confines the
+ * re-raise to this thread; raw syscalls, no libc. */
+ LINUX_SYSCALL(__NR_tgkill,
+ LINUX_SYSCALL(__NR_getpid), LINUX_SYSCALL(__NR_gettid),
+ linux_signum);
+ return;
}
kern_printf("sigexc_handler(%d, %p, %p)\n", linux_signum, info, ctxt);
@@ -359,7 +516,13 @@ void sigexc_handler(int linux_signum, struct linux_siginfo* info, struct linux_u
sizeof(sa.sa_mask));
// Resend signal to self
- LINUX_SYSCALL(__NR_kill, 0, linux_signum);
+ // (darling-lol: tgkill, thread-only -- kill(0, sig) hit
+ // the whole process group and massacred siblings; the
+ // comment always said "self".)
+ LINUX_SYSCALL(__NR_tgkill,
+ LINUX_SYSCALL(__NR_getpid),
+ LINUX_SYSCALL(__NR_gettid),
+ linux_signum);
}
}
}
diff --git a/darling/src/libsystem_kernel/emulation/src/other/mach/lkm.c b/darling/src/libsystem_kernel/emulation/src/other/mach/lkm.c
index 8c6d5d9..6fd1acf 100644
--- a/darling/src/libsystem_kernel/emulation/src/other/mach/lkm.c
+++ b/darling/src/libsystem_kernel/emulation/src/other/mach/lkm.c
@@ -97,6 +97,15 @@ void mach_driver_init(const char** applep)
// we're being called in the child after a fork; let xtrace know about it
_xtrace_postfork_child();
}
+
+#if defined(__x86_64__)
+ /* darling-lol: resolve the host vDSO clock entry now, while the native
+ * loader is known to be usable; clock reads can then happen from signal
+ * handlers and RPC-guarded callers without running dlopen/dlsym. The vDSO
+ * mapping survives fork(), so the cached pointer stays valid in the child. */
+ extern void __darling_vdso_clock_init(void);
+ __darling_vdso_clock_init();
+#endif
}
__attribute__((visibility("default")))
@@ -128,12 +137,20 @@ uint64_t mach_absolute_time(void)
{
struct timespec ts;
uint64_t out;
-
+
+ /* darling-lol: the vDSO copy of the clock is exact and avoids a host
+ * syscall; `clock_gettime` here is the Linux wrapper, which repeats the
+ * vDSO check and otherwise falls back to the opt-in TSC clock and the
+ * syscall. */
+#if defined(__x86_64__)
+ extern int __darling_vdso_clock_gettime(int clockid, struct timespec* res);
+ if (__darling_vdso_clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
+#endif
clock_gettime(CLOCK_MONOTONIC, &ts);
-
+
out = ts.tv_nsec;
out += ts.tv_sec * NSEC_PER_SEC;
-
+
return out;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/bsd_syscall_table.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/bsd_syscall_table.c
index 7d5ef80..46debb8 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/bsd_syscall_table.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/bsd_syscall_table.c
@@ -24,6 +24,7 @@
#include <darling/emulation/xnu_syscall/bsd/impl/fcntl/openat.h>
#include <darling/emulation/xnu_syscall/bsd/impl/guarded/guarded_close_np.h>
#include <darling/emulation/xnu_syscall/bsd/impl/guarded/guarded_kqueue_np.h>
+#include <darling/emulation/xnu_syscall/bsd/impl/guarded/change_fdguard_np.h>
#include <darling/emulation/xnu_syscall/bsd/impl/guarded/guarded_open_np.h>
#include <darling/emulation/xnu_syscall/bsd/impl/hfs/stub.h>
#include <darling/emulation/xnu_syscall/bsd/impl/ioctl/ioctl.h>
@@ -44,6 +45,7 @@
#include <darling/emulation/xnu_syscall/bsd/impl/misc/getrlimit.h>
#include <darling/emulation/xnu_syscall/bsd/impl/misc/getrusage.h>
#include <darling/emulation/xnu_syscall/bsd/impl/misc/iopolicysys.h>
+#include <darling/emulation/xnu_syscall/bsd/impl/misc/mac_syscall.h>
#include <darling/emulation/xnu_syscall/bsd/impl/misc/proc_info.h>
#include <darling/emulation/xnu_syscall/bsd/impl/misc/ptrace.h>
#include <darling/emulation/xnu_syscall/bsd/impl/misc/reboot.h>
@@ -427,6 +429,7 @@ void* __bsd_syscall_table[600] = {
[369] = sys_kevent64,
[372] = sys_thread_selfid,
[374] = sys_kevent_qos,
+ [381] = sys___mac_syscall,
[394] = sys_pselect,
[395] = sys_pselect_nocancel,
[396] = sys_read_nocancel,
@@ -462,6 +465,7 @@ void* __bsd_syscall_table[600] = {
[441] = sys_guarded_open_np,
[442] = sys_guarded_close_np,
[443] = sys_guarded_kqueue_np,
+ [444] = sys_change_fdguard_np,
[461] = sys_getattrlistbulk,
[462] = sys_clonefileat,
[463] = sys_openat,
@@ -483,6 +487,7 @@ void* __bsd_syscall_table[600] = {
[500] = sys_getentropy,
[515] = sys_ulock_wait,
[516] = sys_ulock_wake,
+ [544] = sys_ulock_wait2,
[517] = sys_fclonefileat,
[521] = sys_abort_with_payload,
[524] = sys_setattrlistat,
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_hw.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_hw.c
index f273989..058b985 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_hw.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_hw.c
@@ -9,6 +9,8 @@
#include <darling/emulation/xnu_syscall/bsd/helper/misc/sysctl_proc.h>
#include <darling/emulation/linux_premigration/ext/sys/utsname.h>
#include <darling/emulation/common/simple.h>
+#include <darling/emulation/common/cpu_topology.h>
+#include <darling/emulation/common/cpu_frequency.h>
#include <darling/emulation/common/string.h>
extern kern_return_t mach_port_deallocate(ipc_space_t task, mach_port_name_t name);
@@ -16,6 +18,7 @@ extern kern_return_t host_info(mach_port_name_t host, int itype, void* hinfo, ma
/* Darling specific */
enum {
+ _HW_ACTIVECPU = 26, // hw.activecpu's OID in XNU; unused in this table before
_HW_PHYSICAL_CPU = 1000,
_HW_PHYSICAL_CPU_MAX,
_HW_LOGICAL_CPU,
@@ -24,10 +27,14 @@ enum {
_HW_CPUSUBTYPE,
_HW_CPUTHREADTYPE,
_HW_64BITCAPABLE,
- _HW_CPUFREQUENCY = 15,
+ _HW_CPUFREQUENCY_COMPAT = 15,
+ _HW_CPUFREQUENCY = 1500,
+ _HW_CACHECONFIG = 1200,
+ _HW_CACHESIZE,
};
static sysctl_handler(handle_availcpu);
+static sysctl_handler(handle_ncpu);
static sysctl_handler(handle_physicalcpu);
static sysctl_handler(handle_physicalcpu_max);
static sysctl_handler(handle_logicalcpu);
@@ -39,11 +46,78 @@ static sysctl_handler(handle_cpusubtype);
static sysctl_handler(handle_cputhreadtype);
static sysctl_handler(handle_cpu64bitcapable);
static sysctl_handler(handle_machine);
+static sysctl_handler(handle_model);
static sysctl_handler(handle_cpufrequency);
+static sysctl_handler(handle_cpufrequency_compat);
+static sysctl_handler(handle_cacheconfig);
+static sysctl_handler(handle_cachesize);
+
+/* hw.optional: CPUID leaf, output register (0-3 = eax-edx) and bit */
+static long optional_feature(void* old, unsigned long* oldlen, unsigned int leaf, int reg, int bit)
+{
+ int value = 0;
+
+ sysctl_handle_size(sizeof(int));
+#if defined(__i386__) || defined(__x86_64__)
+ unsigned int regs[4];
+ __asm__("cpuid" : "=a" (regs[0]), "=b" (regs[1]), "=c" (regs[2]), "=d" (regs[3]) : "0" (leaf), "2" (0));
+ value = (regs[reg] >> bit) & 1;
+#endif
+ *((int*) old) = value;
+ return 0;
+}
+
+#define optional_handler(_name, _leaf, _reg, _bit) \
+ static sysctl_handler(handle_optional_##_name) { return optional_feature(old, oldlen, _leaf, _reg, _bit); }
+
+optional_handler(floatingpoint, 1, 3, 0)
+optional_handler(mmx, 1, 3, 23)
+optional_handler(sse, 1, 3, 25)
+optional_handler(sse2, 1, 3, 26)
+optional_handler(sse3, 1, 2, 0)
+optional_handler(supplementalsse3, 1, 2, 9)
+optional_handler(sse4_1, 1, 2, 19)
+optional_handler(sse4_2, 1, 2, 20)
+optional_handler(aes, 1, 2, 25)
+optional_handler(avx1_0, 1, 2, 28)
+optional_handler(rdrand, 1, 2, 30)
+optional_handler(f16c, 1, 2, 29)
+optional_handler(fma, 1, 2, 12)
+optional_handler(avx2_0, 7, 1, 5)
+optional_handler(bmi1, 7, 1, 3)
+optional_handler(bmi2, 7, 1, 8)
+optional_handler(avx512f, 7, 1, 16)
+
+enum {
+ _HW_OPTIONAL = 1100,
+};
+
+static const struct known_sysctl sysctls_hw_optional[] = {
+ { .oid = 1, .type = CTLTYPE_INT, .exttype = "I", .name = "floatingpoint", .handler = handle_optional_floatingpoint },
+ { .oid = 2, .type = CTLTYPE_INT, .exttype = "I", .name = "mmx", .handler = handle_optional_mmx },
+ { .oid = 3, .type = CTLTYPE_INT, .exttype = "I", .name = "sse", .handler = handle_optional_sse },
+ { .oid = 4, .type = CTLTYPE_INT, .exttype = "I", .name = "sse2", .handler = handle_optional_sse2 },
+ { .oid = 5, .type = CTLTYPE_INT, .exttype = "I", .name = "sse3", .handler = handle_optional_sse3 },
+ { .oid = 6, .type = CTLTYPE_INT, .exttype = "I", .name = "supplementalsse3", .handler = handle_optional_supplementalsse3 },
+ { .oid = 7, .type = CTLTYPE_INT, .exttype = "I", .name = "sse4_1", .handler = handle_optional_sse4_1 },
+ { .oid = 8, .type = CTLTYPE_INT, .exttype = "I", .name = "sse4_2", .handler = handle_optional_sse4_2 },
+ { .oid = 9, .type = CTLTYPE_INT, .exttype = "I", .name = "aes", .handler = handle_optional_aes },
+ { .oid = 10, .type = CTLTYPE_INT, .exttype = "I", .name = "avx1_0", .handler = handle_optional_avx1_0 },
+ { .oid = 11, .type = CTLTYPE_INT, .exttype = "I", .name = "rdrand", .handler = handle_optional_rdrand },
+ { .oid = 12, .type = CTLTYPE_INT, .exttype = "I", .name = "f16c", .handler = handle_optional_f16c },
+ { .oid = 13, .type = CTLTYPE_INT, .exttype = "I", .name = "fma", .handler = handle_optional_fma },
+ { .oid = 14, .type = CTLTYPE_INT, .exttype = "I", .name = "avx2_0", .handler = handle_optional_avx2_0 },
+ { .oid = 15, .type = CTLTYPE_INT, .exttype = "I", .name = "bmi1", .handler = handle_optional_bmi1 },
+ { .oid = 16, .type = CTLTYPE_INT, .exttype = "I", .name = "bmi2", .handler = handle_optional_bmi2 },
+ { .oid = 17, .type = CTLTYPE_INT, .exttype = "I", .name = "avx512f", .handler = handle_optional_avx512f },
+ { .oid = 18, .type = CTLTYPE_INT, .exttype = "I", .name = "x86_64", .handler = handle_cpu64bitcapable },
+ { .oid = -1 }
+};
const struct known_sysctl sysctls_hw[] = {
{ .oid = HW_AVAILCPU, .type = CTLTYPE_INT, .exttype = "", .name = "availcpu", .handler = handle_availcpu },
- { .oid = HW_NCPU, .type = CTLTYPE_INT, .exttype = "", .name = "ncpu", .handler = handle_availcpu },
+ { .oid = _HW_ACTIVECPU, .type = CTLTYPE_INT, .exttype = "", .name = "activecpu", .handler = handle_availcpu },
+ { .oid = HW_NCPU, .type = CTLTYPE_INT, .exttype = "", .name = "ncpu", .handler = handle_ncpu },
{ .oid = _HW_PHYSICAL_CPU, .type = CTLTYPE_INT, .exttype = "", .name = "physicalcpu", .handler = handle_physicalcpu },
{ .oid = _HW_PHYSICAL_CPU_MAX, .type = CTLTYPE_INT, .exttype = "", .name = "physicalcpu_max", .handler = handle_physicalcpu_max },
{ .oid = _HW_LOGICAL_CPU, .type = CTLTYPE_INT, .exttype = "", .name = "logicalcpu", .handler = handle_logicalcpu },
@@ -55,7 +129,12 @@ const struct known_sysctl sysctls_hw[] = {
{ .oid = _HW_CPUTHREADTYPE, .type = CTLTYPE_INT, .exttype = "", .name = "cputhreadtype", .handler = handle_cputhreadtype },
{ .oid = _HW_64BITCAPABLE, .type = CTLTYPE_INT, .exttype = "", .name = "cpu64bit_capable", .handler = handle_cpu64bitcapable },
{ .oid = HW_MACHINE, .type = CTLTYPE_STRING, .exttype = "S", .name = "machine", .handler = handle_machine },
- { .oid = _HW_CPUFREQUENCY, .type = CTLTYPE_INT, .exttype = "", .name = "cpufrequency", .handler = handle_cpufrequency },
+ { .oid = _HW_OPTIONAL, .type = CTLTYPE_NODE, .exttype = "", .name = "optional", .subctls = sysctls_hw_optional },
+ { .oid = HW_MODEL, .type = CTLTYPE_STRING, .exttype = "S", .name = "model", .handler = handle_model },
+ { .oid = _HW_CPUFREQUENCY, .type = CTLTYPE_QUAD, .exttype = "Q", .name = "cpufrequency", .handler = handle_cpufrequency },
+ { .oid = _HW_CPUFREQUENCY_COMPAT, .type = CTLTYPE_INT, .exttype = "I", .name = "cpufrequency_compat", .handler = handle_cpufrequency_compat },
+ { .oid = _HW_CACHECONFIG, .type = CTLTYPE_OPAQUE, .exttype = "Q", .name = "cacheconfig", .handler = handle_cacheconfig },
+ { .oid = _HW_CACHESIZE, .type = CTLTYPE_OPAQUE, .exttype = "Q", .name = "cachesize", .handler = handle_cachesize },
{ .oid = -1 }
};
@@ -71,6 +150,32 @@ static const struct host_basic_info* gethostinfo(void)
hself = mach_host_self();
host_info(hself, HOST_BASIC_INFO, &hinfo, &hcount);
mach_port_deallocate(mach_task_self(), hself);
+
+ // Keep the emulated MacBookPro16,1 identity (hw.model, brand string,
+ // memory size), but report the host's real CPU counts. The game and the
+ // clients size their worker pools from these values; the old fixed
+ // 8 logical / 8 physical configuration made the game create 7 workers
+ // on a 12-thread host. Note it did not even match the emulated machine:
+ // a MacBookPro16,1 with the i9-9980HK is 8-core/16-thread.
+ // /tmp/rclol-fixed-cpu-count restores the old values for A/B runs.
+ const struct darling_cpu_topology* topo = darling_host_cpu_topology();
+
+ if (topo->detected && !topo->fixed)
+ {
+ hinfo.avail_cpus = topo->available;
+ hinfo.max_cpus = topo->online;
+ hinfo.physical_cpu = topo->physical_available;
+ hinfo.physical_cpu_max = topo->physical_online;
+ hinfo.logical_cpu = topo->available;
+ hinfo.logical_cpu_max = topo->online;
+ }
+ else
+ {
+ hinfo.avail_cpus = hinfo.max_cpus = DARLING_CPU_FIXED_COUNT;
+ hinfo.physical_cpu = hinfo.physical_cpu_max = DARLING_CPU_FIXED_COUNT;
+ hinfo.logical_cpu = hinfo.logical_cpu_max = DARLING_CPU_FIXED_COUNT;
+ }
+ hinfo.max_mem = 17179869184ULL; // 16 GB, matching the emulated machine
}
return &hinfo;
@@ -83,6 +188,13 @@ sysctl_handler(handle_availcpu)
return 0;
}
+sysctl_handler(handle_ncpu)
+{
+ sysctl_handle_size(sizeof(int));
+ *((int*) old) = gethostinfo()->max_cpus;
+ return 0;
+}
+
sysctl_handler(handle_physicalcpu)
{
sysctl_handle_size(sizeof(int));
@@ -169,16 +281,49 @@ sysctl_handler(handle_cputhreadtype)
return 0;
}
+static long cpu_frequency(uint64_t* frequency)
+{
+ char buffer[32];
+ const char* paths[] = {
+ "/Volumes/SystemRoot/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq",
+ "/Volumes/SystemRoot/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq",
+ "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq",
+ "/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq"
+ };
+ for (unsigned i = 0; i < sizeof(paths)/sizeof(paths[0]); ++i)
+ if (read_string(paths[i], buffer, sizeof(buffer)) && darling_frequency_hz(buffer, frequency)) return 0;
+#if defined(__i386__) || defined(__x86_64__)
+ unsigned int a, b, c, d;
+ __asm__("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "0"(0), "2"(0));
+ if (a >= 0x16) {
+ __asm__("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "0"(0x16), "2"(0));
+ if (a & 0xffff) {
+ *frequency = (uint64_t)(a & 0xffff) * 1000000;
+ return 0;
+ }
+ }
+#endif
+ return -ENOENT;
+}
+
sysctl_handler(handle_cpufrequency)
{
- int freq = 2400000;
- char buf[16];
+ uint64_t frequency;
+ sysctl_handle_size(sizeof(frequency));
+ long result = cpu_frequency(&frequency);
+ if (!result) *((uint64_t*)old) = frequency;
+ return result;
+}
- if (read_string("/sys/bus/cpu/devices/cpu0/cpufreq/cpuinfo_max_freq", buf, sizeof(buf)))
- freq = __simple_atoi(buf, NULL);
- *((int*) old) = freq;
-
- return 0;
+sysctl_handler(handle_cpufrequency_compat)
+{
+ // Numeric HW_CPU_FREQ retains XNU's legacy 32-bit ABI. Modern by-name
+ // queries use the separate 64-bit OID above, as in kern_mib.c.
+ uint64_t frequency;
+ sysctl_handle_size(sizeof(uint32_t));
+ long result = cpu_frequency(&frequency);
+ if (!result) *((uint32_t*)old) = (uint32_t)frequency;
+ return result;
}
sysctl_handler(handle_cpu64bitcapable)
@@ -193,3 +338,104 @@ sysctl_handler(handle_machine)
copyout_string(need_uname()->machine, (char*) old, oldlen);
return 0;
}
+
+sysctl_handler(handle_model)
+{
+ // an Intel Mac model identifier, matching the x86_64 machine we emulate
+ copyout_string("MacBookPro16,1", (char*) old, oldlen);
+ return 0;
+}
+
+#define CACHE_LEVELS 10
+// The guest has no /sys of its own; the host's is reachable through the SystemRoot volume
+#define HOST_CACHE_DIR "/Volumes/SystemRoot/sys/devices/system/cpu/cpu0/cache/"
+
+static unsigned long long parse_ull(const char* s, const char** end)
+{
+ unsigned long long v = 0;
+ while (*s >= '0' && *s <= '9')
+ v = v * 10 + (*s++ - '0');
+ if (end)
+ *end = s;
+ return v;
+}
+
+// Number of CPUs in a sysfs list such as "0-1,4-5" (darling_cpu_count_list, shared with the topology probe)
+
+// Describes the data/unified cache of one level (1 = L1D) of the host's CPU 0.
+// Instruction caches are skipped, as in XNU. Returns false if the host doesn't report the level.
+static bool host_cache_level(unsigned int level, unsigned long long* size, unsigned int* sharing)
+{
+ char path[96], buf[64];
+
+ for (int index = 0; index < 10; index++)
+ {
+ const char* end;
+ unsigned long long value;
+
+ __simple_sprintf(path, HOST_CACHE_DIR "index%d/level", index);
+ if (!read_string(path, buf, sizeof(buf)))
+ break;
+ if (parse_ull(buf, NULL) != level)
+ continue;
+
+ __simple_sprintf(path, HOST_CACHE_DIR "index%d/type", index);
+ if (!read_string(path, buf, sizeof(buf)) || buf[0] == 'I')
+ continue;
+
+ __simple_sprintf(path, HOST_CACHE_DIR "index%d/size", index);
+ if (!read_string(path, buf, sizeof(buf)))
+ continue;
+ value = parse_ull(buf, &end);
+ if (*end == 'K')
+ value <<= 10;
+ else if (*end == 'M')
+ value <<= 20;
+ *size = value;
+
+ __simple_sprintf(path, HOST_CACHE_DIR "index%d/shared_cpu_list", index);
+ *sharing = read_string(path, buf, sizeof(buf)) ? darling_cpu_count_list(buf) : 1;
+ if (*sharing == 0)
+ *sharing = 1;
+ return true;
+ }
+ return false;
+}
+
+// hw.cacheconfig[0] is the number of logical CPUs, [n] the number of CPUs sharing a level n cache
+sysctl_handler(handle_cacheconfig)
+{
+ unsigned long long* config = (unsigned long long*) old;
+
+ sysctl_handle_size(CACHE_LEVELS * sizeof(*config));
+ memset(config, 0, CACHE_LEVELS * sizeof(*config));
+ config[0] = gethostinfo()->logical_cpu_max;
+ for (unsigned int level = 1; level < 4; level++)
+ {
+ unsigned long long size;
+ unsigned int sharing;
+
+ if (host_cache_level(level, &size, &sharing))
+ config[level] = sharing;
+ }
+ return 0;
+}
+
+// hw.cachesize[0] is the memory size, [n] the size in bytes of the level n cache
+sysctl_handler(handle_cachesize)
+{
+ unsigned long long* sizes = (unsigned long long*) old;
+
+ sysctl_handle_size(CACHE_LEVELS * sizeof(*sizes));
+ memset(sizes, 0, CACHE_LEVELS * sizeof(*sizes));
+ sizes[0] = gethostinfo()->max_mem;
+ for (unsigned int level = 1; level < 4; level++)
+ {
+ unsigned long long size;
+ unsigned int sharing;
+
+ if (host_cache_level(level, &size, &sharing))
+ sizes[level] = size;
+ }
+ return 0;
+}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_kern.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_kern.c
index bd9cdc2..83ad15c 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_kern.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_kern.c
@@ -139,18 +139,23 @@ sysctl_handler(handle_boottime)
// Fill in struct timeval { tv_sec, tv_usec }
struct sysinfo info;
struct bsd_timeval* tv;
-
- if (__linux_sysinfo(&info) == -1)
- return -errno;
+ /* darling-lol: the boot time does not change, and libc's clock_gettime(CLOCK_MONOTONIC) asks for it twice per
+ * call (two sysinfo + two gettimeofday syscalls). Computed once per process. */
+ static volatile long boot_sec;
tv = (struct bsd_timeval*) old;
if (tv != NULL)
{
if (*oldlen < sizeof(*tv))
return -EINVAL;
- sys_gettimeofday(tv, NULL);
-
- tv->tv_sec -= info.uptime;
+ if (boot_sec == 0)
+ {
+ if (__linux_sysinfo(&info) == -1)
+ return -errno;
+ sys_gettimeofday(tv, NULL);
+ boot_sec = tv->tv_sec - info.uptime;
+ }
+ tv->tv_sec = boot_sec;
// we don't want the microseconds to be constantly jumping around
// (that actually causes an infinite loop in `_mach_boottime_usec` in libc)
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_machdep.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_machdep.c
index 4319355..122ad3a 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_machdep.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_machdep.c
@@ -4,6 +4,7 @@
#include <alloca.h>
#include <darling/emulation/common/simple.h>
+#include <darling/emulation/common/cpu_topology.h>
#include <darling/emulation/conversion/fcntl/open.h>
#include <darling/emulation/xnu_syscall/bsd/impl/fcntl/open.h>
#include <darling/emulation/xnu_syscall/bsd/helper/misc/readline.h>
@@ -25,6 +26,7 @@ enum {
_CPU_BRAND_STRING,
_CPU_FEATURES,
_CPU_CORE_COUNT,
+ _CPU_THREAD_COUNT,
};
static sysctl_handler(handle_vendor);
@@ -32,9 +34,15 @@ static sysctl_handler(handle_max_basic);
static sysctl_handler(handle_family);
static sysctl_handler(handle_model);
static sysctl_handler(handle_stepping);
-static sysctl_handler(handle_brand_string);
-static sysctl_handler(handle_features);
+static sysctl_handler(handle_brand_string)
+{
+ copyout_string("Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz", (char*) old, oldlen);
+ return 0;
+}
+
+sysctl_handler(handle_features);
static sysctl_handler(handle_core_count);
+static sysctl_handler(handle_thread_count);
const struct known_sysctl sysctls_machdep_cpu[] = {
{ .oid = _CPU_MAX_BASIC, .type = CTLTYPE_INT, .exttype = "I", .name = "max_basic", .handler = handle_max_basic },
@@ -42,9 +50,10 @@ const struct known_sysctl sysctls_machdep_cpu[] = {
{ .oid = _CPU_FAMILY, .type = CTLTYPE_INT, .exttype = "I", .name = "family", .handler = handle_family },
{ .oid = _CPU_MODEL, .type = CTLTYPE_INT, .exttype = "I", .name = "model", .handler = handle_model },
{ .oid = _CPU_STEPPING, .type = CTLTYPE_INT, .exttype = "I", .name = "stepping", .handler = handle_stepping },
- { .oid = _CPU_BRAND_STRING, .type = CTLTYPE_STRING, .exttype = "S", .name = "vendor", .handler = handle_brand_string },
+ { .oid = _CPU_BRAND_STRING, .type = CTLTYPE_STRING, .exttype = "S", .name = "brand_string", .handler = handle_brand_string },
{ .oid = _CPU_FEATURES, .type = CTLTYPE_STRING, .exttype = "S", .name = "features", .handler = handle_features },
{ .oid = _CPU_CORE_COUNT, .type = CTLTYPE_INT, .exttype = "I", .name = "core_count", .handler = handle_core_count },
+ { .oid = _CPU_THREAD_COUNT, .type = CTLTYPE_INT, .exttype = "I", .name = "thread_count", .handler = handle_thread_count },
{ .oid = -1 }
};
@@ -154,38 +163,37 @@ sysctl_handler(handle_stepping)
return 0;
}
+// Original cpuid-based implementation, replaced by the fixed brand string defined above
+#if 0
sysctl_handler(handle_brand_string)
{
- setup(0x80000000);
-
- __cpuid(level,eax,ebx, ecx, edx);
-
- if (eax < 0x80000004) // the information is not implemented
- return 2;
-
+ union
+ {
+ unsigned int brand[12];
+ char name[49];
+ } v;
+ unsigned int eax, ebx, ecx, edx;
+ __asm__("cpuid" : "=a" (eax), "=b" (ebx), "=c" (ecx), "=d" (edx) : "0" (0x80000000u), "2" (0));
- union
- {
- unsigned int brand[12];
- char name[49];
- } v;
+ if (eax < 0x80000004) // the information is not implemented
+ return -ENOENT;
- for (int i = 1; i < 4; i++)
- {
- eax = 0x80000000+i;
- __cpuid(level, eax, ebx, ecx, edx);
- v.brand[0x0+(i-1)*4] = eax;
- v.brand[0x1+(i-1)*4] = ebx;
- v.brand[0x2+(i-1)*4] = ecx;
- v.brand[0x3+(i-1)*4] = edx;
- }
+ for (unsigned int i = 0; i < 3; i++)
+ {
+ __asm__("cpuid" : "=a" (eax), "=b" (ebx), "=c" (ecx), "=d" (edx) : "0" (0x80000002u + i), "2" (0));
+ v.brand[i * 4 + 0] = eax;
+ v.brand[i * 4 + 1] = ebx;
+ v.brand[i * 4 + 2] = ecx;
+ v.brand[i * 4 + 3] = edx;
+ }
- v.name[48] = 0;
- copyout_string(v.name, (char*) old, oldlen);
+ v.name[48] = 0;
+ copyout_string(v.name, (char*) old, oldlen);
- return 0;
+ return 0;
}
+#endif
sysctl_handler(handle_features)
{
@@ -269,45 +277,54 @@ sysctl_handler(handle_features)
// TODO: i doubt core count is ever going to change.
// this is something that could be cached if we had some way of doing something only once in libsystem_kernel (i.e. a simple version of dispatch_once).
sysctl_handler(handle_core_count) {
- // it's easier to use /proc/cpuinfo for this.
- // using cpuid directly is more difficult because AMD processors don't use the same method as Intel processors.
+ // the topology probe (shared with the hw.* sysctls) reads the host's
+ // package/core ids and caches the answer; fall back to /proc/cpuinfo when
+ // the host doesn't expose /sys.
+ const struct darling_cpu_topology* topo = darling_host_cpu_topology();
+ size_t core_count = (topo->detected && !topo->fixed) ? topo->physical_online : 0;
+ int infofd = -1;
- int infofd = sys_open_nocancel("/proc/cpuinfo", BSD_O_RDONLY, 0);
- struct rdline_buffer rbuf;
- const char* line;
+ if (core_count == 0) {
+ // it's easier to use /proc/cpuinfo for this.
+ // using cpuid directly is more difficult because AMD processors don't use the same method as Intel processors.
- // a reasonable default
- size_t core_count = 1;
+ infofd = sys_open_nocancel("/proc/cpuinfo", BSD_O_RDONLY, 0);
+ struct rdline_buffer rbuf;
+ const char* line;
- if (infofd < 0)
- goto out;
+ // a reasonable default
+ core_count = 1;
- _readline_init(&rbuf);
+ if (infofd < 0)
+ goto out;
- while ((line = _readline(infofd, &rbuf)) != NULL) {
- if (strncmp(line, "cpu cores", 9) != 0) {
- continue;
- }
+ _readline_init(&rbuf);
- // great, we've got "cpu cores"
- // now find the actual number
- while (*line != ':' && *line != '\0')
- ++line;
+ while ((line = _readline(infofd, &rbuf)) != NULL) {
+ if (strncmp(line, "cpu cores", 9) != 0) {
+ continue;
+ }
- // shouldn't happen, but just in case
- if (*line == '\0')
- continue;
+ // great, we've got "cpu cores"
+ // now find the actual number
+ while (*line != ':' && *line != '\0')
+ ++line;
- ++line; // skip the colon
- if (*line == '\0')
- continue;
+ // shouldn't happen, but just in case
+ if (*line == '\0')
+ continue;
- ++line; // skip the space
- if (*line == '\0')
- continue;
+ ++line; // skip the colon
+ if (*line == '\0')
+ continue;
- core_count = __simple_atoi(line, NULL);
- break;
+ ++line; // skip the space
+ if (*line == '\0')
+ continue;
+
+ core_count = __simple_atoi(line, NULL);
+ break;
+ }
}
out:
@@ -319,3 +336,21 @@ out:
*oldlen = sizeof(uint32_t);
return 0;
}
+
+// The host's online logical CPU count (machdep.cpu.thread_count on macOS).
+sysctl_handler(handle_thread_count) {
+ const struct darling_cpu_topology* topo = darling_host_cpu_topology();
+ size_t thread_count = (topo->detected && !topo->fixed) ? topo->online : 0;
+
+ if (thread_count == 0) {
+ // Detection failed or the opt-out is active: fall back to the same
+ // fixed value the hw.* handlers use.
+ thread_count = DARLING_CPU_FIXED_COUNT;
+ }
+
+ if (old != NULL && oldlen && *oldlen >= sizeof(uint32_t))
+ *(uint32_t*)old = thread_count;
+ if (oldlen)
+ *oldlen = sizeof(uint32_t);
+ return 0;
+}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_net.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_net.c
index 8fabdda..6cc204e 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_net.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_net.c
@@ -22,6 +22,7 @@ static sysctl_handler(handle_route);
static int do_iflist(void* old, unsigned long* oldlen);
#define LINUX_SIOCGIFCONF 0x8912
+#define LINUX_SIOCGIFFLAGS 0x8913
#define LINUX_SIOCGIFHWADDR 0x8927
#define LINUX_SIOCGIFBRDADDR 0x8919
#define LINUX_SIOCGIFNETMASK 0x891b
@@ -144,6 +145,16 @@ static int do_iflist(void* old, unsigned long* oldlen) {
memcpy(dl->sdl_data, curr->lifr_name, name_len);
+ /* Interface-list consumers (including Chromium) discard adapters
+ * without IFF_UP. Populate actual flags instead of leaving zero.
+ * The low ten flags share values; Linux multicast differs. */
+ memcpy(tmp.lifr_name, curr->lifr_name, sizeof(curr->lifr_name));
+ if ((status = __real_ioctl(tmp_sock, LINUX_SIOCGIFFLAGS, &tmp)) < 0)
+ goto out;
+ unsigned short linux_flags = (unsigned short)tmp.lifr_flags;
+ ifm->ifm_flags = linux_flags & 0x03ff;
+ if (linux_flags & 0x1000) ifm->ifm_flags |= IFF_MULTICAST;
+
// grab the MAC
memcpy(tmp.lifr_name, curr->lifr_name, sizeof(curr->lifr_name));
if ((status = __real_ioctl(tmp_sock, LINUX_SIOCGIFHWADDR, &tmp)) < 0)
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_sysctl.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_sysctl.c
index 5a49882..5dc5fa7 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_sysctl.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_sysctl.c
@@ -7,12 +7,15 @@
enum {
_PROC_CPUTYPE = 1000,
+ _PROC_TRANSLATED,
};
static sysctl_handler(handle_proc_cputype);
+static sysctl_handler(handle_proc_translated);
const struct known_sysctl sysctls_sysctl[] = {
{ .oid = _PROC_CPUTYPE, .type = CTLTYPE_INT, .exttype = "I", .name = "proc_cputype", .handler = handle_proc_cputype },
+ { .oid = _PROC_TRANSLATED, .type = CTLTYPE_INT, .exttype = "I", .name = "proc_translated", .handler = handle_proc_translated },
{ .oid = -1 }
};
@@ -44,3 +47,11 @@ sysctl_handler(handle_proc_cputype)
return 0;
}
+
+sysctl_handler(handle_proc_translated)
+{
+ // guest code runs natively on the host CPU, never under binary translation
+ sysctl_handle_size(sizeof(int));
+ *((int*) old) = 0;
+ return 0;
+}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/audit/audit_addr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/audit/audit_addr.c
index 304dadd..1320eda 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/audit/audit_addr.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/audit/audit_addr.c
@@ -1,6 +1,6 @@
#include <darling/emulation/xnu_syscall/bsd/impl/audit/audit_addr.h>
-#include <os/lock.h>
+#include <darling/emulation/common/private_mutex.h>
#include <darling/emulation/xnu_syscall/bsd/impl/unistd/geteuid.h>
#include <darling/emulation/xnu_syscall/bsd/impl/misc/getentropy.h>
@@ -18,19 +18,19 @@ static auditinfo_addr_t info = {
.ai_flags = 0,
};
// should be a rw lock but *shrug*
-static os_unfair_lock info_lock = OS_UNFAIR_LOCK_INIT;
+static emulation_mutex info_lock = EMULATION_MUTEX_INITIALIZER;
long sys_getaudit_addr(struct auditinfo_addr* auditinfo_addr, int length) {
- os_unfair_lock_lock(&info_lock);
+ emulation_mutex_lock(&info_lock);
memcpy(auditinfo_addr, &info, min(length, sizeof(auditinfo_addr_t)));
- os_unfair_lock_unlock(&info_lock);
+ emulation_mutex_unlock(&info_lock);
return 0;
};
long sys_setaudit_addr(struct auditinfo_addr* auditinfo_addr, int length) {
- os_unfair_lock_lock(&info_lock);
+ emulation_mutex_lock(&info_lock);
memcpy(&info, auditinfo_addr, min(length, sizeof(auditinfo_addr_t)));
@@ -39,6 +39,6 @@ long sys_setaudit_addr(struct auditinfo_addr* auditinfo_addr, int length) {
sys_getentropy(&info.ai_asid, sizeof(info.ai_asid));
}
- os_unfair_lock_unlock(&info_lock);
+ emulation_mutex_unlock(&info_lock);
return 0;
};
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_create.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_create.c
index 6f8cf43..c5f6d08 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_create.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_create.c
@@ -33,6 +33,7 @@ static bool _uses_threads = false;
// http://www.tldp.org/FAQ/Threads-FAQ/clone.c
+
static void rpc_guard(int fd) {
guard_entry_options_t options;
options.close = __dserver_close_socket;
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_register.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_register.c
index 919205b..5c678ab 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_register.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_register.c
@@ -34,6 +34,29 @@ struct _pthread_registration_data {
static struct _pthread_registration_data g_pth_regdata;
+extern int __darling_is_league_game(void);
+extern void __darling_core_isolate_main_thread(void);
+extern void __darling_core_isolate_new_thread(void);
+/* Modern libpthread registration has stack_addr_hint at byte 36, followed
+ * by mutex_default_policy at byte 44. Keep the old prefix ABI unchanged. */
+/* darling-lol: the League game process takes FIRSTFIT mutexes, whose contention the process handles itself
+ * (see psynch_mutexwait.c) and whose condition variables then use the ulock path. This is decided once per
+ * process: on for the game unless /tmp/rclol-no-local-condvars exists, and /tmp/rclol-local-condvars-all turns
+ * it on for every process (probe tests). /tmp/rclol-local-condvars used to be the opt-in switch; the file may
+ * still exist and is now ignored. */
+static void enable_local_conditions(const struct _pthread_registration_data* data,
+ unsigned long length)
+{
+ if (length < 48 || data->version < 48) return;
+ if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-local-condvars-all", 0) != 0 &&
+ (LINUX_SYSCALL(__NR_access, "/tmp/rclol-no-local-condvars", 0) == 0 ||
+ !__darling_is_league_game())) return;
+ /* FIRSTFIT (3) | USE_ULOCK (0x100). Recursive/error-check/shared mutexes
+ * still take libpthread's psynch path. This field is a kernel copyout. */
+ uint32_t policy = 3u | 0x100u;
+ __builtin_memcpy((char*)(uintptr_t)data + 44, &policy, sizeof(policy));
+}
+
long sys_bsdthread_register(void* thread_start, void* wqthread, int pthsize,
const struct _pthread_registration_data* pth_regdata,
unsigned long pth_regdata_len, unsigned long long dpq_offset)
@@ -42,6 +65,12 @@ long sys_bsdthread_register(void* thread_start, void* wqthread, int pthsize,
pthread_entry_point = (bsdthread_entry_t) thread_start;
wqueue_entry_point = (bsdwqthread_entry_t) wqthread;
g_pth_regdata = *pth_regdata;
+ enable_local_conditions(pth_regdata, pth_regdata_len);
+ /* darling-lol: libpthread initializes here, on the process' initial thread, before any thread can be
+ * created. That is the one place where the game process' main thread can be given its own core
+ * scheduling cookie (see mach_traps.c). The call decides for itself whether this is the game, whether
+ * the caller is the initial thread and whether the host supports the prctl. */
+ __darling_core_isolate_main_thread();
uintptr_t pthread_handle = _pthread_getspecific_direct(_PTHREAD_TSD_SLOT_PTHREAD_SELF);
uintptr_t dispatch_qaddr = pthread_handle + g_pth_regdata.tsd_offset + g_pth_regdata.dispatch_queue_offset;
@@ -59,6 +88,10 @@ long sys_bsdthread_register(void* thread_start, void* wqthread, int pthsize,
void pthread_entry_point_wrapper(void* self, int thread_port, void* funptr,
void* funarg, unsigned long stack_addr, unsigned int flags)
{
+ /* darling-lol: every guest thread starts here; it must not keep the core scheduling cookie it
+ * inherited from its creator (a thread created by the main thread would carry the main cookie and
+ * could run on the main thread's core). See mach_traps.c. */
+ __darling_core_isolate_new_thread();
sigexc_thread_setup();
uintptr_t pthread_handle = (unsigned long) self;
@@ -122,6 +155,9 @@ void pthread_entry_point_wrapper(void* self, int thread_port, void* funptr,
void wqueue_entry_point_wrapper(void* self, int thread_port, void* stackaddr,
void* item, int reuse, int nevents)
{
+ /* darling-lol: workqueue threads start here too and must lose the inherited core scheduling
+ * cookie the same way normal pthreads do (see mach_traps.c). */
+ __darling_core_isolate_new_thread();
sigexc_thread_setup();
wqueue_entry_point_asm_jump(self, thread_port, stackaddr, item, reuse, nevents);
}
@@ -156,6 +192,11 @@ void wqueue_entry_point_asm_jump(void* self, int thread_port, void* stackaddr,
// `_start_wqthread` does not expect the stack to be aligned (using the
// call instruction will add the return address to the stack, causing
// it to be aligned).
+ // Workqueue entry is a fresh kernel-style jump, including on reuse.
+ // self sits immediately above the usable stack. Dropping the old
+ // syscall frames prevents every reuse from consuming another frame.
+ "movq %%rdi, %%rsp\n"
+ "xorq %%rbp, %%rbp\n"
"jmpq *%[wqueue_entry_point]\n"
::
// Arguments follow the usual x86_64 calling conventions.
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_terminate.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_terminate.c
index 14a8238..7c95cdf 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_terminate.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_terminate.c
@@ -1,3 +1,5 @@
+#include <darling/emulation/common/simple.h>
+#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
#include <darling/emulation/xnu_syscall/bsd/impl/bsdthread/bsdthread_terminate.h>
#include <sys/errno.h>
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/pthread_canceled.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/pthread_canceled.c
index 1e78300..a6acb59 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/pthread_canceled.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/pthread_canceled.c
@@ -12,12 +12,24 @@
long sys_pthread_canceled(int action)
{
+ static unsigned unsupported_actions;
+ unsigned action_bit = (unsigned)action <= 2 ? 1U << action : 0;
+
if (action == 0 && !uses_threads())
return -EINVAL;
+ /* Cache only a server-confirmed unsupported action. Cancellation state
+ * itself must never be cached: a server implementing it still receives
+ * every request. The server implementation is fixed for this process's
+ * lifetime. Atomic access keeps the shared capability cache race-free. */
+ if (__atomic_load_n(&unsupported_actions, __ATOMIC_RELAXED) & action_bit)
+ return -ENOSYS;
+
int ret = dserver_rpc_pthread_canceled(action);
if (ret < 0)
ret = errno_linux_to_bsd(ret);
+ if (ret == -ENOSYS && action_bit)
+ __atomic_fetch_or(&unsupported_actions, action_bit, __ATOMIC_RELAXED);
return ret;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/workq_kernreturn.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/workq_kernreturn.c
index dbcdd98..0021dc2 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/workq_kernreturn.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/workq_kernreturn.c
@@ -3,7 +3,7 @@
#include <sys/errno.h>
#include <stddef.h>
#include <sys/queue.h>
-#include <os/lock.h>
+#include <darling/emulation/common/private_mutex.h>
#include <pthread/tsd_private.h>
#define __PTHREAD_EXPOSE_INTERNALS__ 1
@@ -61,7 +61,7 @@
#define WORKQ_EXIT_THREAD_NKEVENT (-1)
static int workq_sem = WQ_MAX_THREADS; // max 64 threads in use
-static os_unfair_lock workq_parked_lock = OS_UNFAIR_LOCK_INIT;
+static emulation_mutex workq_parked_lock = EMULATION_MUTEX_INITIALIZER;
// static int workq_parked_threads = 0; // num spawned, but unused threads
static int workq_parked[WQ_MAX_THREADS];
static int workq_parked_prio[WQ_MAX_THREADS];
@@ -78,14 +78,41 @@ struct parked_thread
static void list_add(struct parked_thread* head, struct parked_thread* item);
static void list_remove(struct parked_thread* head, struct parked_thread* item);
+// Parked-thread nodes live in a fixed pool, not on the parked thread's own stack. The list is shared by
+// all workers, and a node on a stack that goes away while a neighbour is still linked turns the next
+// removal into a write to unmapped memory. Callers hold workq_parked_lock.
+#define WQ_NODE_POOL_SIZE (WQ_MAX_THREADS * 2)
+static struct parked_thread workq_node_pool[WQ_NODE_POOL_SIZE];
+static unsigned char workq_node_used[WQ_NODE_POOL_SIZE];
+
+static struct parked_thread* workq_node_alloc(void)
+{
+ for (int i = 0; i < WQ_NODE_POOL_SIZE; i++)
+ {
+ if (!workq_node_used[i])
+ {
+ workq_node_used[i] = 1;
+ workq_node_pool[i].sem = 0;
+ workq_node_pool[i].flags = 0;
+ workq_node_pool[i].event = NULL;
+ return &workq_node_pool[i];
+ }
+ }
+ return NULL;
+}
+
+static void workq_node_free(struct parked_thread* node)
+{
+ if (node >= &workq_node_pool[0] && node < &workq_node_pool[WQ_NODE_POOL_SIZE])
+ workq_node_used[node - &workq_node_pool[0]] = 0;
+}
+
struct timespec
{
long tv_sec;
long tv_nsec;
};
-void __attribute__((weak)) os_unfair_lock_unlock(os_unfair_lock_t lock) {}
-void __attribute__((weak)) os_unfair_lock_lock(os_unfair_lock_t lock) {}
//void* __attribute__((weak)) __attribute__((visibility("default"))) pthread_getspecific(unsigned long key) { return NULL; }
//int __attribute__((weak)) __attribute__((visibility("default"))) pthread_setspecific(unsigned long key, const void* value) { return 1; }
@@ -95,6 +122,7 @@ static int priority_to_class(int prio);
// This is horrible, but it may work
static struct wq_kevent_data* wq_event_pending = NULL;
+
static void rpc_guard(int fd) {
guard_entry_options_t options;
options.close = __dserver_close_socket;
@@ -162,14 +190,24 @@ long sys_workq_kernreturn(int options, void* item, int affinity, int prio)
// and can be either recycled or terminated
int thread_self;
- struct parked_thread me;
+ struct parked_thread stack_me; // only used if the node pool is exhausted
+ struct parked_thread* me;
+ struct wq_kevent_data* resume_event = NULL;
+ int resume_flags;
dthread_t dthread;
bool terminating = false;
- os_unfair_lock_lock(&workq_parked_lock);
+ emulation_mutex_lock(&workq_parked_lock);
+
+ me = workq_node_alloc();
+ if (me == NULL)
+ {
+ me = &stack_me;
+ me->event = NULL;
+ }
// Semaphore locked state (wait for wakeup)
- me.sem = 0;
+ me->sem = 0;
// extract initial flags
// (in case we only get created and used once and then terminate; `_pthread_wqthread` requires a valid `flags` argument)
@@ -177,33 +215,37 @@ long sys_workq_kernreturn(int options, void* item, int affinity, int prio)
prio = _pthread_getspecific_direct(_PTHREAD_TSD_SLOT_PTHREAD_QOS_CLASS);
// doesn't extract `WQ_FLAG_THREAD_KEVENT` if we had it set, but that shouldn't matter
// like i said before, the only case where we actually need these flags to be set here is when the thread is going to die immediately after creation
- me.flags = extract_wq_flags(prio);
+ me->flags = extract_wq_flags(prio);
// Enqueue for future WQOPS_QUEUE_REQTHREADS
- TAILQ_INSERT_HEAD(&workq_parked_head, &me, entries);
+ TAILQ_INSERT_HEAD(&workq_parked_head, me, entries);
// Decrease the amount of running threads
sem_up(&workq_sem);
- os_unfair_lock_unlock(&workq_parked_lock);
+ emulation_mutex_unlock(&workq_parked_lock);
// Wait until someone calls WQOPS_QUEUE_REQTHREADS
// and wakes us up
- if (!sem_down(&me.sem, 15))
+ if (!sem_down(&me->sem, 15))
{
// Make sure we haven't just been woken up before locking the queue
// and remove us from the queue if not.
- os_unfair_lock_lock(&workq_parked_lock);
+ emulation_mutex_lock(&workq_parked_lock);
- if (me.sem > 0)
+ if (me->sem > 0)
{
- os_unfair_lock_unlock(&workq_parked_lock);
+ emulation_mutex_unlock(&workq_parked_lock);
goto wakeup;
}
- TAILQ_REMOVE(&workq_parked_head, &me, entries);
+ TAILQ_REMOVE(&workq_parked_head, me, entries);
- os_unfair_lock_unlock(&workq_parked_lock);
+ resume_event = NULL;
+ resume_flags = me->flags;
+ workq_node_free(me);
+
+ emulation_mutex_unlock(&workq_parked_lock);
terminating = true;
@@ -214,8 +256,15 @@ long sys_workq_kernreturn(int options, void* item, int affinity, int prio)
wakeup: // we actually want to go back into the thread to do work
// __simple_printf("Thread %d woken up, prio=%d\n", thread_self, me.flags & WQ_FLAG_THREAD_PRIOMASK);
- if (me.event)
- wq_event_pending = me.event;
+ // the requester already took us off the list; the node is ours to read, then to give back
+ emulation_mutex_lock(&workq_parked_lock);
+ resume_event = me->event;
+ resume_flags = me->flags;
+ workq_node_free(me);
+ emulation_mutex_unlock(&workq_parked_lock);
+
+ if (resume_event)
+ wq_event_pending = resume_event;
resume_thread: // we want the thread to resume, but it might be just to die
// reset stack and call entry point again with WQ_FLAG_THREAD_REUSE
@@ -223,9 +272,9 @@ resume_thread: // we want the thread to resume, but it might be just to die
dthread = _pthread_getspecific_direct(_PTHREAD_TSD_SLOT_PTHREAD_SELF);
wqueue_entry_point_asm_jump(dthread, thread_self, dthread->stackbottom,
- (!terminating && me.event) ? me.event->events : NULL,
- me.flags | WQ_FLAG_THREAD_REUSE,
- terminating ? WORKQ_EXIT_THREAD_NKEVENT : (me.event ? me.event->nevents : 0)
+ (!terminating && resume_event) ? resume_event->events : NULL,
+ resume_flags | WQ_FLAG_THREAD_REUSE,
+ terminating ? WORKQ_EXIT_THREAD_NKEVENT : (resume_event ? resume_event->nevents : 0)
);
__builtin_unreachable();
@@ -257,7 +306,7 @@ resume_thread: // we want the thread to resume, but it might be just to die
// Increase the amount of running threads
sem_down(&workq_sem, -1);
- os_unfair_lock_lock(&workq_parked_lock);
+ emulation_mutex_lock(&workq_parked_lock);
if (workq_parked_head.tqh_first != NULL)
{
@@ -276,12 +325,12 @@ resume_thread: // we want the thread to resume, but it might be just to die
// Resume the thread
sem_up(&thread->sem);
- os_unfair_lock_unlock(&workq_parked_lock);
+ emulation_mutex_unlock(&workq_parked_lock);
continue;
}
- os_unfair_lock_unlock(&workq_parked_lock);
+ emulation_mutex_unlock(&workq_parked_lock);
// __simple_printf("Spawning a new thread, nevents=%d\n", (wq_event != NULL) ? wq_event->nevents : -1);
wq_event_pending = wq_event;
@@ -320,36 +369,52 @@ resume_thread: // we want the thread to resume, but it might be just to die
#endif
}
+/* Keep a nonnegative token count. The old decrement/negative-marker/plain
+ * store sequence could overwrite a concurrent post, losing a worker wakeup.
+ * EAGAIN and EINTR are retry conditions, not worker retirement requests. */
int sem_down(int* sem, int timeout)
{
- int result;
- struct timespec ts = { timeout, 0 };
-
-try_again:
- result = __sync_add_and_fetch(sem, -1);
- if (result < 0)
- {
- *sem = -1;
- result = __linux_futex(sem, FUTEX_WAIT, -1, (timeout != -1) ? &ts : NULL, 0, 0);
-
- if (result == -1)
- return 0;
-
- goto try_again;
- }
-
- return 1;
+ struct timespec deadline = {0, 0};
+ if (timeout >= 0) {
+ LINUX_SYSCALL(__NR_clock_gettime, 1 /* CLOCK_MONOTONIC */, &deadline);
+ deadline.tv_sec += timeout;
+ }
+ for (;;) {
+ int value = __atomic_load_n(sem, __ATOMIC_ACQUIRE);
+ while (value > 0) {
+ if (__atomic_compare_exchange_n(sem, &value, value - 1, 0,
+ __ATOMIC_ACQUIRE, __ATOMIC_RELAXED))
+ return 1;
+ }
+ struct timespec remaining;
+ if (timeout >= 0) {
+ struct timespec now;
+ LINUX_SYSCALL(__NR_clock_gettime, 1 /* CLOCK_MONOTONIC */, &now);
+ remaining.tv_sec = deadline.tv_sec - now.tv_sec;
+ remaining.tv_nsec = deadline.tv_nsec - now.tv_nsec;
+ if (remaining.tv_nsec < 0) {
+ --remaining.tv_sec;
+ remaining.tv_nsec += 1000000000;
+ }
+ if (remaining.tv_sec < 0 ||
+ (remaining.tv_sec == 0 && remaining.tv_nsec == 0))
+ return 0;
+ }
+ long result = LINUX_SYSCALL(__NR_futex, sem, FUTEX_WAIT, 0,
+ timeout >= 0 ? &remaining : NULL, NULL, 0);
+ if (result == -110 /* ETIMEDOUT */)
+ return 0;
+ if (result < 0 && result != -11 /* EAGAIN */ && result != -4 /* EINTR */)
+ return 0;
+ }
}
void sem_up(int* sem)
{
- int result;
- result = __sync_add_and_fetch(sem, 1);
- if (result == 0)
- {
- *sem = 1;
- __linux_futex(sem, FUTEX_WAKE, 1, NULL, 0, 0);
- }
+ __atomic_fetch_add(sem, 1, __ATOMIC_RELEASE);
+ /* Wake on every post: several waiters can be asleep while tokens are
+ * posted faster than the first awakened waiter can consume them. */
+ LINUX_SYSCALL(__NR_futex, sem, FUTEX_WAKE, 1, NULL, NULL, 0);
}
#define __PTHREAD_PRIORITY_CBIT_USER_INTERACTIVE 0x20
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/change_fdguard_np.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/change_fdguard_np.c
new file mode 100644
index 0000000..0bb5800
--- /dev/null
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/change_fdguard_np.c
@@ -0,0 +1,13 @@
+#include <darling/emulation/xnu_syscall/bsd/impl/guarded/change_fdguard_np.h>
+
+#include <darling/emulation/common/base.h>
+
+/* darling-lol M57: Chromium's UDP socket code calls change_fdguard_np
+ * (Darwin syscall 444). Darling's fdguard support is a no-op everywhere
+ * (guarded_open_np/guarded_close_np/guarded_kqueue_np all ignore it), so
+ * accept the request and report success. Without this the syscall was
+ * unhandled (ENOSYS), which made Chromium FATAL in udp_socket_posix.cc. */
+long sys_change_fdguard_np(int fd, uint64_t* guard, uint64_t* nguard, unsigned guardflags, int* fdflagsp)
+{
+ return 0;
+}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_close_np.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_close_np.c
index 8980d77..361efc4 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_close_np.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_close_np.c
@@ -7,6 +7,8 @@
long sys_guarded_close_np(int fd, uint64_t* guardid)
{
- *guardid = 0;
+ /* darling-lol M57: callers can pass a NULL or otherwise unusable guardid
+ * pointer; writing it crashed (SEGV on a read-only page). The guard id is
+ * not used by Darling's no-op fdguard implementation, so don't write it. */
return sys_close(fd);
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_kqueue_np.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_kqueue_np.c
index bdedda8..1992482 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_kqueue_np.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_kqueue_np.c
@@ -7,6 +7,6 @@
long sys_guarded_kqueue_np(uint64_t* guardid, unsigned guardflags)
{
- *guardid = 0;
+ /* darling-lol M57: do not write guardid (may be unusable). */
return sys_kqueue();
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_open_np.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_open_np.c
index 0fe8a0f..5ae6ad0 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_open_np.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_open_np.c
@@ -7,6 +7,6 @@
long sys_guarded_open_np(const char* filename, uint64_t* guardid, unsigned int guardflags, int flags, unsigned int mode)
{
- *guardid = 0;
+ /* darling-lol M57: do not write guardid (may be unusable). */
return sys_open(filename, flags, mode);
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent.c
index 15d7b43..dea29b8 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent.c
@@ -3,7 +3,8 @@
#include <stddef.h>
#include <sys/errno.h>
-#include <darling/emulation/common/base.h>
+#include <darling/emulation/common/simple.h>
+#define LOLSYS(fmt, ...) do { } while (0)
#include <darling/emulation/conversion/errno.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
@@ -13,6 +14,7 @@ long sys_kevent(int kq, const struct kevent *changelist, int nchanges,
struct kevent *eventlist, int nevents,
const struct timespec *timeout)
{
+ LOLSYS("sys_kevent entered");
int ret = kevent_impl(kq, changelist, nchanges, eventlist, nevents, timeout);
if (ret < 0)
ret = -errno;
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent64.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent64.c
index dd9bb0d..137f319 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent64.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent64.c
@@ -3,7 +3,8 @@
#include <stddef.h>
#include <sys/errno.h>
-#include <darling/emulation/common/base.h>
+#include <darling/emulation/common/simple.h>
+#define LOLSYS(fmt, ...) do { } while (0)
#include <darling/emulation/conversion/errno.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
@@ -13,6 +14,7 @@ long sys_kevent64(int kq, const struct kevent64_s *changelist, int nchanges,
struct kevent64_s *eventlist, int nevents, unsigned int flags,
const struct timespec *timeout)
{
+ LOLSYS("sys_kevent64 entered");
int ret = kevent64_impl(kq, changelist, nchanges, eventlist, nevents, flags, timeout);
if (ret < 0)
ret = -errno;
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent_qos.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent_qos.c
index 3f7cba6..bed91d7 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent_qos.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent_qos.c
@@ -4,7 +4,8 @@
#include <sys/errno.h>
#include <sys/fcntl.h>
-#include <darling/emulation/common/base.h>
+#include <darling/emulation/common/simple.h>
+#define LOLSYS(fmt, ...) do { } while (0)
#include <darling/emulation/conversion/errno.h>
#include <darling/emulation/common/simple.h>
#include <darling/emulation/xnu_syscall/bsd/impl/fcntl/fcntl.h>
@@ -15,6 +16,9 @@
static int default_kq = -1;
+extern void* malloc(__SIZE_TYPE__ size);
+extern void free(void* ptr);
+
extern void* memmove(void* dst, const void* src, __SIZE_TYPE__ n);
static void kevent_qos_to_64(const struct kevent_qos_s *ev, struct kevent64_s* ev64);
@@ -29,6 +33,11 @@ long sys_kevent_qos(int kq, const struct kevent_qos_s *changelist, int nchanges,
struct kevent64_s* eventlist64 = NULL;
struct wq_kevent_data wq_kevent;
struct timespec polling_timeout = {};
+ struct kevent64_s small_changes[16], small_events[16];
+ struct kevent_qos_s workq_event;
+ void* allocated_changes = NULL;
+ void* allocated_events = NULL;
+ if(nchanges < 0 || nevents < 0) return -EINVAL;
if ((kq == -1) != !!(flags & KEVENT_FLAG_WORKQ))
return -EINVAL;
@@ -43,17 +52,28 @@ long sys_kevent_qos(int kq, const struct kevent_qos_s *changelist, int nchanges,
{
int i;
- changelist64 = (struct kevent64_s*) __builtin_alloca(nchanges * sizeof(struct kevent64_s));
+ if(nchanges <= 16) changelist64=small_changes;
+ else {
+ allocated_changes=malloc((size_t)nchanges*sizeof(struct kevent64_s));
+ if(!allocated_changes) return -ENOMEM;
+ changelist64=allocated_changes;
+ }
for (i = 0; i < nchanges; i++)
kevent_qos_to_64(&changelist[i], &changelist64[i]);
}
- if (eventlist != NULL && nevents > 0)
- eventlist64 = (struct kevent64_s*) __builtin_alloca(nevents * sizeof(struct kevent64_s));
+ if (eventlist != NULL && nevents > 0) {
+ if(nevents <= 16) eventlist64=small_events;
+ else {
+ allocated_events=malloc((size_t)nevents*sizeof(struct kevent64_s));
+ if(!allocated_events) { free(allocated_changes);return -ENOMEM; }
+ eventlist64=allocated_events;
+ }
+ }
if (nevents == 0 && (flags & KEVENT_FLAG_WORKQ))
{
- eventlist = (struct kevent_qos_s*) __builtin_alloca(nevents * sizeof(struct kevent_qos_s));
+ eventlist = &workq_event;
nevents = 1;
- eventlist64 = (struct kevent64_s*) __builtin_alloca(nevents * sizeof(struct kevent64_s));
+ eventlist64 = small_events;
}
rv = sys_kevent64(kq, changelist64, nchanges, eventlist64, nevents, flags, (flags & KEVENT_FLAG_IMMEDIATE) ? &polling_timeout : NULL);
@@ -83,6 +103,8 @@ long sys_kevent_qos(int kq, const struct kevent_qos_s *changelist, int nchanges,
// __simple_printf("workq done processing, returning %d to caller\n", rv);
}
+ free(allocated_events);
+ free(allocated_changes);
return rv;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kqueue.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kqueue.c
index e808529..bd663f6 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kqueue.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kqueue.c
@@ -3,7 +3,8 @@
#include <stddef.h>
#include <sys/errno.h>
-#include <darling/emulation/common/base.h>
+#include <darling/emulation/common/simple.h>
+#define LOLSYS(fmt, ...) do { } while (0)
#include <darling/emulation/conversion/errno.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
#include <darling/emulation/common/simple.h>
@@ -16,6 +17,7 @@ int __attribute__((weak)) __attribute__((visibility("default"))) kqueue_impl(voi
long sys_kqueue(void)
{
+ LOLSYS("sys_kqueue entered");
int ret = kqueue_impl();
if (ret < 0)
ret = -errno;
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makefd.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makefd.c
index 1afb57e..d6896e6 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makefd.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makefd.c
@@ -3,14 +3,20 @@
#include <darling/emulation/common/base.h>
#include <darling/emulation/conversion/errno.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
-#include <darling/emulation/other/mach/lkm.h>
+#include <darlingserver/rpc.h>
+
+extern int dserver_rpc_fileport_makefd(unsigned int port_name, int* fd);
long sys_fileport_makefd(int port)
{
- int ret = lkm_call(NR_fileport_makefd, (void*)(unsigned long) port);
+ int fd = -1;
+ int ret;
- if (ret < 0)
- return errno_linux_to_bsd(ret);
+ /* darling-lol M54: see fileport_makeport.c. */
+ ret = dserver_rpc_fileport_makefd((unsigned int)port, &fd);
+ if (ret < 0) {
+ return -ret;
+ }
- return ret;
+ return fd;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makeport.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makeport.c
index be2926c..a25882e 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makeport.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makeport.c
@@ -3,20 +3,22 @@
#include <darling/emulation/common/base.h>
#include <darling/emulation/conversion/errno.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
-#include <darling/emulation/other/mach/lkm.h>
+#include <darlingserver/rpc.h>
+
+extern int dserver_rpc_fileport_makeport(int fd, unsigned int* port_name);
long sys_fileport_makeport(int fd, int* port)
{
- struct fileport_makeport_args args;
-
- args.fd = fd;
- args.port_out = 0;
-
- int ret = lkm_call(NR_fileport_makeport, &args);
+ unsigned int port_name = 0;
+ int ret;
- if (ret < 0)
- return errno_linux_to_bsd(ret);
+ /* darling-lol M54: the old LKM API is gone (its stub UD2s). Route
+ * through darlingserver, which holds the FD in a fileport kobject. */
+ ret = dserver_rpc_fileport_makeport(fd, &port_name);
+ if (ret < 0) {
+ return -ret;
+ }
- *port = args.port_out;
+ *port = (int)port_name;
return 0;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/mac_syscall.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/mac_syscall.c
new file mode 100644
index 0000000..479294a
--- /dev/null
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/mac_syscall.c
@@ -0,0 +1,53 @@
+#include <darling/emulation/xnu_syscall/bsd/impl/misc/mac_syscall.h>
+
+#include <darling/emulation/common/base.h>
+#include <darling/emulation/common/simple.h>
+#include <darling/emulation/conversion/duct_errno.h>
+
+extern int memcmp(const void* s1, const void* s2, __SIZE_TYPE__ n);
+extern __SIZE_TYPE__ strlen(const char* s);
+
+long sys___mac_syscall(const char* policy, int call, void* arg)
+{
+ char name[32];
+ __SIZE_TYPE__ i;
+
+ /* darling-lol: syscall 381 was unmapped (ENOSYS), and Chromium's
+ * seatbelt client CHECKs it, killing every helper/GPU process with a
+ * silent UD2. There is no MAC framework to back this; for the
+ * "Seatbelt" policy we fail soft with success (consistent with the
+ * sandbox_init stubs that report success without enforcement).
+ * Anything else keeps ENOSYS. Guest memory is directly readable here;
+ * bound the copy. */
+
+ if (policy == NULL) {
+ return -EINVAL;
+ }
+
+ for (i = 0; i < sizeof(name) - 1; i++) {
+ char c = policy[i];
+ name[i] = c;
+ if (c == '\0') {
+ break;
+ }
+ }
+ name[sizeof(name) - 1] = '\0';
+
+ if (memcmp(name, "Seatbelt", 9) == 0) {
+ __simple_printf("mac_syscall: Seatbelt call %d (arg=%p), no enforcement, succeeding\n",
+ call, arg);
+ return 0;
+ }
+
+ if (memcmp(name, "AMFI", 5) == 0) {
+ /* darling-lol: same posture as Seatbelt (Chromium's sandbox code
+ * probes AMFI status via ___sandbox_ms; ENOSYS trips its CHECK).
+ * No code-signing enforcement exists to report on. */
+ __simple_printf("mac_syscall: AMFI call %d (arg=%p), no enforcement, succeeding\n",
+ call, arg);
+ return 0;
+ }
+
+ __simple_printf("mac_syscall: unsupported policy \"%s\" call %d\n", name, call);
+ return -ENOSYS;
+}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/proc_info.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/proc_info.c
index c2f70fb..af904ed 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/proc_info.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/proc_info.c
@@ -83,6 +83,38 @@ long sys_proc_info(uint32_t callnum, int32_t pid, uint32_t flavor,
case 2: // proc_pidinfo
return _proc_pidinfo(pid, flavor, arg, buffer, bufsize);
case 3: // proc_pidfdinfo
+ {
+ if (flavor == PROC_PIDFDVNODEPATHINFO)
+ {
+ struct vnode_fdinfowithpath* info = (struct vnode_fdinfowithpath*) buffer;
+ struct vchroot_unexpand_args uargs;
+ char procpath[64];
+ long len;
+
+ if (!buffer)
+ return -EFAULT;
+ if (bufsize < (int32_t)sizeof(*info))
+ return -ENOSPC;
+
+ memset(buffer, 0, bufsize);
+
+ /* Resolve /proc/<pid>/fd/<fd> to the emulated path. */
+ __simple_sprintf(procpath, "/proc/%d/fd/%d", pid, (int)arg);
+ len = sys_readlink(procpath, uargs.path, sizeof(uargs.path) - 1);
+ if (len < 0)
+ return errno_linux_to_bsd(len);
+ uargs.path[len] = 0;
+
+ if (vchroot_unexpand(&uargs) != 0)
+ return -EINVAL;
+
+ strncpy(info->pvip.vip_path, uargs.path, sizeof(info->pvip.vip_path) - 1);
+ return sizeof(*info);
+ }
+ __simple_printf("sys_proc_info(): Unsupported pidfdinfo flavor: %d\n",
+ flavor);
+ return -ENOTSUP;
+ }
case 1: // proc_listpids
default:
__simple_printf("sys_proc_info(): Unsupported callnum: %d\n",
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/ptrace.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/ptrace.c
index 0a8f1e4..a19f12e 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/ptrace.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/ptrace.c
@@ -22,7 +22,7 @@ extern int getpid(void);
long sys_ptrace(int request, int pid, void* addr, int data)
{
const char* cmd = NULL;
- int ret;
+ int ret = -EINVAL;
switch (request)
{
@@ -171,6 +171,17 @@ long sys_ptrace(int request, int pid, void* addr, int data)
// return ret;
cmd = "PT_THUPDATE"; break;
}
+ case XNU_PT_DENY_ATTACH:
+ {
+ // Apple semantics: this process can no longer be traced or
+ // debugged; always succeeds. Darling has no kernel debugger
+ // support to disable, but the call itself must report success:
+ // anti-tamper code treats any other return as failure.
+ // (darling-lol: LeagueClient dies right after this call when
+ // it returns stack garbage instead of 0.)
+ ret = 0;
+ cmd = "PT_DENY_ATTACH"; break;
+ }
}
if (cmd != NULL)
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/sysctl.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/sysctl.c
index 10f39a5..29747d4 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/sysctl.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/sysctl.c
@@ -42,11 +42,15 @@ const struct known_sysctl sysctls_root = {
void copyout_string(const char* str, char* out, unsigned long* out_len)
{
- unsigned long len;
+ // as on XNU, the reported length includes the terminating NUL
+ unsigned long len = strlen(str) + 1;
+
if (out != NULL)
- len = strlcpy(out, str, *out_len);
- else
- len = strlen(str);
+ {
+ strlcpy(out, str, *out_len);
+ if (len > *out_len)
+ len = *out_len;
+ }
if (out_len != NULL)
*out_len = len;
@@ -130,7 +134,10 @@ long sys_sysctlbyname(const char* name, unsigned long namelen, void* old, unsign
}
if (next == NULL)
+ {
+ __simple_kprintf("sysctlbyname: unknown name %s", name);
return -ENOENT;
+ }
current = next;
token = strtok_r(NULL, ".", &saveptr);
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/mman/madvise.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/mman/madvise.c
index 10c412b..adb7e24 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/mman/madvise.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/mman/madvise.c
@@ -4,23 +4,97 @@
#include <darling/emulation/conversion/errno.h>
#include <darling/emulation/common/base.h>
+#include <darling/emulation/common/simple.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
+#include <darling/emulation/xnu_syscall/bsd/helper/misc/sysctl_proc.h>
#define MADV_FREE 5
#define MADV_FREE_REUSABLE 7
#define MADV_FREE_REUSE 8
+#define LINUX_MADV_FREE 8
+#define LINUX_MADV_POPULATE_WRITE 23
+
+/* darling-lol: Linux splits a transparent huge page when MADV_FREE covers only
+ * part of it. libmalloc hands the free hint out per 4 KiB page of its tiny,
+ * small and medium regions, i.e. for the mappings that otherwise get 2 MiB
+ * huge pages (measured on the target host: one 4 KiB hint turned 2 MiB of
+ * AnonHugePages into 512 small pages). Forwarding the hint only for whole huge
+ * pages stays inside the Darwin contract (a hint may cover less than the
+ * requested range) and keeps the huge pages.
+ *
+ * The host policy is read once per process: with THP "always" every anonymous
+ * mapping gets huge pages, so partial hints are skipped; with "never" or
+ * "madvise" there are no huge pages for this layer to split (the emulation
+ * never requests MADV_HUGEPAGE), so the full range is forwarded as before.
+ * An unreadable policy keeps the old full-range behaviour. */
+static unsigned long thp_huge_page_size; /* written before thp_mode */
+static int thp_mode = -1; /* -1 not probed, 0 full range, 1 whole huge pages only */
+
+static int host_thp_mode(void)
+{
+ int mode = __atomic_load_n(&thp_mode, __ATOMIC_ACQUIRE);
+ char buffer[64];
+
+ if (mode >= 0)
+ return mode;
+
+ mode = 0;
+ if (read_string("/sys/kernel/mm/transparent_hugepage/enabled", buffer, sizeof(buffer)))
+ {
+ const char* selected;
+
+ /* the selected policy is the one in brackets */
+ for (selected = buffer; *selected != '\0' && *selected != '['; ++selected)
+ ;
+ if (selected[0] == '[' && selected[1] == 'a') /* [always] */
+ mode = 1;
+ }
+ if (mode == 1 && read_string("/sys/kernel/mm/transparent_hugepage/hpage_pmd_size", buffer, sizeof(buffer)))
+ {
+ unsigned long size = __simple_atoi(buffer, 0);
+ if (size >= 4096 && (size & (size - 1)) == 0)
+ thp_huge_page_size = size;
+ }
+ __atomic_store_n(&thp_mode, mode, __ATOMIC_RELEASE);
+ return mode;
+}
long sys_madvise(void* addr, unsigned long len, int advice)
{
int ret;
- if (advice == MADV_FREE_REUSABLE || advice == MADV_FREE_REUSE)
- advice = MADV_FREE;
+ if (advice == MADV_FREE || advice == MADV_FREE_REUSABLE) {
+ if (host_thp_mode() == 1) {
+ unsigned long page = thp_huge_page_size;
+ unsigned long lo, hi;
- // advice < 0 are identical between OS X and Linux
- // advice >= 5 are specific to OS X/BSD
- if (advice >= 5)
+ if (page == 0)
+ page = 2 * 1024 * 1024;
+ /* round the range inward to whole huge pages */
+ lo = ((unsigned long) addr + page - 1) & ~(page - 1);
+ hi = ((unsigned long) addr + len) & ~(page - 1);
+ if (hi <= lo)
+ return 0; /* no whole huge page: nothing to split */
+ addr = (void*) lo;
+ len = hi - lo;
+ }
+ // The allocator no longer needs these private anonymous pages. Linux
+ // may reclaim them without swapping; subsequent writes cancel reclaim.
+ advice = LINUX_MADV_FREE;
+ } else if (advice == MADV_FREE_REUSE) {
+ // Reused pages must stop being lazily discardable. A write-prefault
+ // cancels MADV_FREE without changing the surviving page contents.
+ // This operation is available on Linux 5.14 and later.
+ //
+ // The full range is forwarded on purpose: Darwin promises that the
+ // range is no longer lazily reclaimable when REUSE returns, and a
+ // caller may reuse a page range that an earlier free hint marked.
+ // Only a write-prefault of that particular page cancels its laziness.
+ advice = LINUX_MADV_POPULATE_WRITE;
+ } else if (advice >= 5) {
+ // Other OS X/BSD-specific hints do not share Linux's numeric values.
return 0;
+ }
ret = LINUX_SYSCALL(__NR_madvise, addr, len, advice);
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/accept.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/accept.c
index 4daa631..5aeedd0 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/accept.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/accept.c
@@ -34,9 +34,16 @@ long sys_accept_nocancel(int fd, void* from, int* socklen)
ret = errno_linux_to_bsd(ret);
else
{
- fixed = (struct sockaddr_fixup*) from;
- fixed->bsd_family = sfamily_linux_to_bsd(fixed->linux_family);
- fixed->bsd_length = *socklen;
+ /* darling-lol M57: accept(fd, NULL, NULL) is legal on macOS (the
+ * caller isn't interested in the peer address). Chromium's socket
+ * code does this; dereferencing NULL crashed in
+ * sys_accept_nocancel. */
+ if (from != 0 && socklen != 0)
+ {
+ fixed = (struct sockaddr_fixup*) from;
+ fixed->bsd_family = sfamily_linux_to_bsd(fixed->linux_family);
+ fixed->bsd_length = *socklen;
+ }
// on macOS (and BSD), accept-ed sockets inherit O_NONBLOCK
int parent_flags = sys_fcntl_nocancel(fd, F_GETFL, 0);
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvfrom.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvfrom.c
index 1419bed..ea83e75 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvfrom.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvfrom.c
@@ -11,6 +11,8 @@
#include <darling/emulation/xnu_syscall/bsd/helper/bsdthread/cancelable.h>
extern void *memcpy(void *dest, const void *src, __SIZE_TYPE__ n);
+extern void rclol_trace_io(const char* op, int fd, long ret);
+
long sys_recvfrom(int fd, void* buf, unsigned long len,
int flags, void* from, int* socklen)
@@ -31,8 +33,11 @@ long sys_recvfrom_nocancel(int fd, void* buf, unsigned long len,
ret = LINUX_SYSCALL(__NR_socketcall, LINUX_SYS_RECVFROM, ((long[6]) { fd, buf, len,
flags, from, socklen }));
#else
- ret = LINUX_SYSCALL(__NR_recvfrom, fd, buf, len, flags, from, socklen);
+ /* this passed the unconverted BSD flags: BSD MSG_DONTWAIT (0x80) is Linux MSG_EOR and BSD MSG_WAITALL (0x40) is
+ * Linux MSG_DONTWAIT. sendto/sendmsg/recvmsg already used the converted value. */
+ ret = LINUX_SYSCALL(__NR_recvfrom, fd, buf, len, linux_flags, from, socklen);
#endif
+ rclol_trace_io("recvfrom", fd, ret);
if (ret < 0)
ret = errno_linux_to_bsd(ret);
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvmsg.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvmsg.c
index 927ccbb..ae353d3 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvmsg.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvmsg.c
@@ -17,6 +17,8 @@ extern void* memcpy(void* dest, const void* src, __SIZE_TYPE__ len);
#define ALIGN(what, how) ( ((what) + (how) - 1) & ~((how) - 1) )
+extern void rclol_trace_io(const char* op, int fd, long ret);
+
long sys_recvmsg(int socket, struct bsd_msghdr* msg, int flags)
{
CANCELATION_POINT();
@@ -62,6 +64,7 @@ long sys_recvmsg_nocancel(int socket, struct bsd_msghdr* msg, int flags)
#else
ret = LINUX_SYSCALL(__NR_recvmsg, socket, &lmsg, linux_flags);
#endif
+ rclol_trace_io("recvmsg", socket, ret);
if (ret < 0)
ret = errno_linux_to_bsd(ret);
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/process/fork.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/process/fork.c
index 76b6f53..a6c24ff 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/process/fork.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/process/fork.c
@@ -4,6 +4,7 @@
#include <darling/emulation/common/base.h>
#include <darling/emulation/conversion/errno.h>
+#include <darling/emulation/conversion/duct_errno.h>
#include <darling/emulation/conversion/signal/duct_signals.h>
#include <darling/emulation/common/bsdthread/per_thread_wd.h>
#include <darling/emulation/xnu_syscall/bsd/impl/unistd/fchdir.h>
@@ -18,18 +19,21 @@
extern _libkernel_functions_t _libkernel_functions;
-long sys_fork(void)
+/* The guest syscall trampoline does not promise ELF stack alignment. */
+__attribute__((force_align_arg_pointer)) long sys_fork(void)
{
int ret;
int wdfd = get_perthread_wd();
-#ifdef SYS_fork
- ret = LINUX_SYSCALL(__NR_fork);
-#else
- ret = LINUX_SYSCALL(__NR_clone, LINUX_SIGCHLD, 0);
-#endif
- if (ret < 0)
- ret = errno_linux_to_bsd(ret);
+ /* The child immediately uses host malloc while refreshing RPC sockets.
+ * A raw fork inherits locked glibc arenas from vanished host threads.
+ * Use the ELF fork, whose atfork machinery prepares those arenas and
+ * the loader's socket bitmap. Guest libsystem callbacks remain separate. */
+ extern struct elf_calls* elfcalls(void);
+ ret = elfcalls()->fork();
+ if (ret < 0) {
+ ret = errno_linux_to_bsd(-elfcalls()->get_errno());
+ }
else if (ret == 0)
{
// in the child
@@ -39,6 +43,11 @@ long sys_fork(void)
// that should also take care of closing descriptors for any other threads.
guard_table_postfork_child();
+ /* darling-lol: the child's IPC space is fresh (duct-tape ipc_task_init creates an empty one),
+ * so local Mach semaphores inherited from the parent must not be served in the child. */
+ extern void __darling_local_semaphores_after_fork(void);
+ __darling_local_semaphores_after_fork();
+
// create a new dserver RPC socket
__dserver_per_thread_socket_refresh();
int newReadFd = __dserver_process_lifetime_pipe_refresh();
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_cvwait.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_cvwait.c
index d093b73..b5d0ab1 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_cvwait.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_cvwait.c
@@ -6,12 +6,28 @@
#include <darling/emulation/conversion/duct_errno.h>
#include <darlingserver/rpc.h>
+#include <darling/emulation/common/slowwait.h>
long sys_psynch_cvwait(void* cv, uint64_t cvlsgen, uint32_t cvugen, void * mutex, uint64_t mugen,
uint32_t flags, int64_t sec, uint32_t nsec)
{
uint32_t retval;
+ {
+ /* darling-lol: the kernel drops the mutex for the caller before it waits (kern_synch.c _psynch_cvwait). A mutex
+ * whose waiters are queued in this process (psynch_mutexwait.c) has to be dropped here; the server knows
+ * nothing about it. */
+ extern int __darling_local_mutex_usable(uint32_t flags);
+ extern uint32_t __darling_local_mutexdrop(void* mutex, uint32_t mgen, uint32_t ugen, uint32_t flags);
+ if (mutex != 0 && __darling_local_mutex_usable(flags)) {
+ __darling_local_mutexdrop(mutex, (uint32_t) (mugen & 0xffffffffu), (uint32_t) (mugen >> 32), flags);
+ mutex = 0;
+ }
+ }
+ unsigned long long slowStart = slowwait_enabled() ? slowwait_now() : 0;
int ret = dserver_rpc_psynch_cvwait(cv, cvlsgen, cvugen, mutex, mugen, flags, sec, nsec, &retval);
+ if (slowStart) {
+ slowwait_log("psynch_cvwait", (unsigned int)(unsigned long) cv, slowStart, (void**) __builtin_frame_address(0), flags, (unsigned int) sec);
+ }
if (ret < 0) {
if (ret == -LINUX_EINTR) {
@@ -21,5 +37,8 @@ long sys_psynch_cvwait(void* cv, uint64_t cvlsgen, uint32_t cvugen, void * mutex
__simple_abort();
}
- return (ret) ? ret : retval;
+ /* RPC ret is a Darwin errno (possibly carrying condition-variable
+ * cleanup flags), not successful sequence bits. The syscall trampoline
+ * recognizes negative errors and sets errno for libpthread's -1 path. */
+ return (ret) ? -(long)ret : (long)retval;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexdrop.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexdrop.c
index 1799f60..d6f4879 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexdrop.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexdrop.c
@@ -8,6 +8,11 @@
long sys_psynch_mutexdrop(void* mutex, uint32_t mgen, uint32_t ugen, uint64_t tid, uint32_t flags)
{
+ extern int __darling_local_mutex_usable(uint32_t flags);
+ extern uint32_t __darling_local_mutexdrop(void* mutex, uint32_t mgen, uint32_t ugen, uint32_t flags);
+ if (__darling_local_mutex_usable(flags))
+ return __darling_local_mutexdrop(mutex, mgen, ugen, flags);
+
uint32_t retval;
int ret = dserver_rpc_psynch_mutexdrop(mutex, mgen, ugen, tid, flags, &retval);
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexwait.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexwait.c
index 2a05251..205eaa6 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexwait.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexwait.c
@@ -7,11 +7,274 @@
#include <darling/emulation/conversion/duct_errno.h>
#include <darlingserver/rpc.h>
+#include <darling/emulation/common/slowwait.h>
+
+/* darling-lol: contended first-fit pthread mutexes handled inside the process.
+ *
+ * libpthread locks and unlocks an uncontended mutex in user space. When threads collide, the loser calls
+ * psynch_mutexwait to sleep and the unlocker calls psynch_mutexdrop to hand the lock on; both were round trips to
+ * darlingserver (~45 us each, with the caller asleep). In League the hot one is the game's own allocator mutex, taken
+ * by the main thread and the workers all the time.
+ *
+ * This is the kernel's algorithm for the FIRSTFIT policy (libpthread/kern/kern_synch.c: _psynch_mutexwait,
+ * _psynch_mutexdrop_internal, ksyn_queue_insert/remove_item, update_low_high) with the same sequence bookkeeping and the
+ * same return values, minus what only matters inside a kernel (owner tracking for priority inheritance, interruptible
+ * waits). A waiter sleeps on a futex. Fair-share and process-shared mutexes, condition variables and rwlocks still go
+ * to the server; psynch_cvwait performs the mutex drop here first when the mutex is a local one.
+ *
+ * Decided once per process: in the League game process this is on unless the host file /tmp/rclol-no-local-mutex
+ * exists. (/tmp/rclol-local-mutex used to be the opt-in switch and is now a no-op.) /tmp/rclol-local-mutex-all
+ * still enables it in every process (probe tests). */
+#define LMTX_COUNT_MASK 0xffffff00u
+#define LMTX_KBIT 0x01u
+#define LMTX_EBIT 0x02u
+#define LMTX_MTX_WAIT 0x20u
+#define LMTX_POLICY_MASK 0x1c0u
+#define LMTX_POLICY_FIRSTFIT 0x80u
+#define LMTX_PSHARED_MASK 0x30u
+#define LMTX_PSHARED 0x10u
+#define LMTX_BUCKETS 256u
+#define LMTX_POOL 4096u
+
+struct lmtx_waiter {
+ struct lmtx_waiter* next;
+ uint32_t lockseq;
+ uint32_t retval;
+ volatile int woken;
+};
+
+struct lmtx {
+ struct lmtx* next; /* bucket chain / free list */
+ unsigned long addr;
+ struct lmtx_waiter* head;
+ struct lmtx_waiter* tail;
+ uint32_t inqueue;
+ uint32_t prepost;
+ uint32_t firstnum, lastnum; /* sequence of the first and of the last queued waiter, as the kernel keeps them */
+ uint32_t lowseq, highseq;
+};
+
+static struct lmtx lmtx_pool[LMTX_POOL];
+static struct lmtx* lmtx_free;
+static unsigned int lmtx_pool_used;
+static struct lmtx* lmtx_buckets[LMTX_BUCKETS];
+static volatile int lmtx_locks[LMTX_BUCKETS];
+static volatile int lmtx_pool_lock;
+static volatile int lmtx_state; /* 0 undecided, 1 on, -1 off */
+static volatile int lmtx_pid;
+
+extern int __darling_is_league_game(void);
+
+static inline int lmtx_seq_lower(uint32_t x, uint32_t y)
+{
+ x &= LMTX_COUNT_MASK; y &= LMTX_COUNT_MASK;
+ return x < y ? (y - x) < (LMTX_COUNT_MASK / 2) : (x - y) > (LMTX_COUNT_MASK / 2);
+}
+
+static inline int lmtx_seq_higher(uint32_t x, uint32_t y)
+{
+ x &= LMTX_COUNT_MASK; y &= LMTX_COUNT_MASK;
+ return x > y ? (x - y) < (LMTX_COUNT_MASK / 2) : (y - x) > (LMTX_COUNT_MASK / 2);
+}
+
+/* Three-state lock (0 free, 1 held, 2 held with sleepers). A thread that has slept must take the lock as "2": it
+ * cannot know whether others are still asleep, and taking it as "1" made the next unlock skip the wake-up and left
+ * them sleeping forever (the first version of this did exactly that and hung the stress test). */
+static void lmtx_lock(volatile int* lock)
+{
+ int c = 0;
+ for (int spin = 0; spin < 64; ++spin) {
+ c = 0;
+ if (__atomic_compare_exchange_n(lock, &c, 1, 0, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED))
+ return;
+ __asm__ volatile("pause");
+ }
+ if (c != 2)
+ c = __atomic_exchange_n(lock, 2, __ATOMIC_ACQUIRE);
+ while (c != 0) {
+ LINUX_SYSCALL(__NR_futex, lock, 0 /* FUTEX_WAIT */, 2, 0UL, 0UL, 0);
+ c = __atomic_exchange_n(lock, 2, __ATOMIC_ACQUIRE);
+ }
+}
+
+static void lmtx_unlock(volatile int* lock)
+{
+ if (__atomic_exchange_n(lock, 0, __ATOMIC_RELEASE) == 2)
+ LINUX_SYSCALL(__NR_futex, lock, 1 /* FUTEX_WAKE */, 1, 0UL, 0UL, 0);
+}
+
+static inline unsigned int lmtx_bucket(unsigned long addr)
+{
+ return (unsigned int) ((addr >> 4) * 2654435761u) >> 24;
+}
+
+__attribute__((visibility("hidden")))
+int __darling_local_mutex_usable(uint32_t flags)
+{
+ int state = __atomic_load_n(&lmtx_state, __ATOMIC_ACQUIRE);
+ if (state == 0) {
+ if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-local-mutex-all", 0) == 0)
+ state = 1;
+ else if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-no-local-mutex", 0) != 0 && __darling_is_league_game())
+ state = 1;
+ else
+ state = -1;
+ __atomic_store_n(&lmtx_pid, (int) LINUX_SYSCALL(__NR_getpid), __ATOMIC_RELAXED);
+ __atomic_store_n(&lmtx_state, state, __ATOMIC_RELEASE);
+ }
+ if (state != 1)
+ return 0;
+ if ((flags & LMTX_POLICY_MASK) != LMTX_POLICY_FIRSTFIT || (flags & LMTX_PSHARED_MASK) == LMTX_PSHARED)
+ return 0;
+ /* a forked child starts with empty queues: the threads recorded as waiting only exist in the parent */
+ int pid = (int) LINUX_SYSCALL(__NR_getpid);
+ if (__atomic_load_n(&lmtx_pid, __ATOMIC_RELAXED) != pid) {
+ for (unsigned int i = 0; i < LMTX_BUCKETS; ++i) { lmtx_buckets[i] = 0; lmtx_locks[i] = 0; }
+ lmtx_free = 0; lmtx_pool_used = 0; lmtx_pool_lock = 0;
+ __atomic_store_n(&lmtx_pid, pid, __ATOMIC_RELAXED);
+ }
+ return 1;
+}
+
+/* caller holds the bucket lock */
+static struct lmtx* lmtx_find(unsigned int bucket, unsigned long addr, int create)
+{
+ struct lmtx* m;
+ for (m = lmtx_buckets[bucket]; m; m = m->next)
+ if (m->addr == addr)
+ return m;
+ if (!create)
+ return 0;
+ lmtx_lock(&lmtx_pool_lock);
+ m = lmtx_free;
+ if (m)
+ lmtx_free = m->next;
+ else if (lmtx_pool_used < LMTX_POOL)
+ m = &lmtx_pool[lmtx_pool_used++];
+ lmtx_unlock(&lmtx_pool_lock);
+ if (!m) {
+ __simple_printf("darling-lol: local mutex table exhausted (%u entries)\n", LMTX_POOL);
+ __simple_abort();
+ }
+ m->addr = addr;
+ m->head = m->tail = 0;
+ m->inqueue = m->prepost = 0;
+ m->firstnum = m->lastnum = m->lowseq = m->highseq = 0;
+ m->next = lmtx_buckets[bucket];
+ lmtx_buckets[bucket] = m;
+ return m;
+}
+
+/* caller holds the bucket lock; an entry with no waiters and no pending hand-off is back in its initial state */
+static void lmtx_release_if_unused(unsigned int bucket, struct lmtx* m)
+{
+ if (m->inqueue != 0 || m->prepost != 0)
+ return;
+ struct lmtx** link = &lmtx_buckets[bucket];
+ while (*link && *link != m)
+ link = &(*link)->next;
+ if (*link)
+ *link = m->next;
+ m->addr = 0;
+ lmtx_lock(&lmtx_pool_lock);
+ m->next = lmtx_free;
+ lmtx_free = m;
+ lmtx_unlock(&lmtx_pool_lock);
+}
+
+__attribute__((visibility("hidden")))
+uint32_t __darling_local_mutexdrop(void* mutex, uint32_t mgen, uint32_t ugen, uint32_t flags)
+{
+ const unsigned long addr = (unsigned long) mutex;
+ const unsigned int bucket = lmtx_bucket(addr);
+ (void) mgen; (void) ugen; (void) flags;
+ lmtx_lock(&lmtx_locks[bucket]);
+ struct lmtx* m = lmtx_find(bucket, addr, 1);
+ if (m->inqueue == 0) {
+ /* nobody is queued yet: the contender that made the unlocker come here is still on its way */
+ m->prepost++;
+ lmtx_unlock(&lmtx_locks[bucket]);
+ return 0;
+ }
+ const uint32_t updatebits = (m->highseq & LMTX_COUNT_MASK) | (LMTX_EBIT | LMTX_KBIT);
+ struct lmtx_waiter* w = m->head;
+ m->head = w->next;
+ if (!m->head)
+ m->tail = 0;
+ if (--m->inqueue > 0) {
+ /* ksyn_queue_remove_item: first/last sequence from the queue ends, then low/high */
+ m->firstnum = m->head->lockseq;
+ m->lastnum = m->tail->lockseq;
+ if (m->lowseq == w->lockseq) m->lowseq = m->firstnum;
+ if (m->highseq == w->lockseq) m->highseq = m->lastnum;
+ } else {
+ m->firstnum = m->lastnum = m->lowseq = m->highseq = 0;
+ }
+ w->retval = updatebits;
+ volatile int* wake = &w->woken;
+ lmtx_release_if_unused(bucket, m);
+ __atomic_store_n(wake, 1, __ATOMIC_RELEASE); /* the waiter's stack frame may be gone right after this */
+ lmtx_unlock(&lmtx_locks[bucket]);
+ LINUX_SYSCALL(__NR_futex, wake, 1 /* FUTEX_WAKE */, 1, 0UL, 0UL, 0);
+ return 0;
+}
+
+__attribute__((visibility("hidden")))
+uint32_t __darling_local_mutexwait(void* mutex, uint32_t mgen, uint32_t ugen, uint32_t flags)
+{
+ const unsigned long addr = (unsigned long) mutex;
+ const unsigned int bucket = lmtx_bucket(addr);
+ const uint32_t lseq = mgen & LMTX_COUNT_MASK;
+ (void) ugen; (void) flags;
+ lmtx_lock(&lmtx_locks[bucket]);
+ struct lmtx* m = lmtx_find(bucket, addr, 1);
+ if (m->prepost) {
+ /* an unlock already arrived for a contender: take it */
+ m->prepost--;
+ uint32_t updatebits = (m->inqueue == 0 ? lseq : (m->highseq & LMTX_COUNT_MASK)) | (LMTX_KBIT | LMTX_EBIT);
+ updatebits &= ~LMTX_MTX_WAIT;
+ lmtx_release_if_unused(bucket, m);
+ lmtx_unlock(&lmtx_locks[bucket]);
+ return updatebits;
+ }
+ struct lmtx_waiter self;
+ self.next = 0;
+ self.lockseq = lseq;
+ self.retval = 0;
+ self.woken = 0;
+ /* ksyn_queue_insert, FIRSTFIT: arrival order */
+ if (m->inqueue == 0) {
+ m->head = m->tail = &self;
+ m->firstnum = m->lastnum = lseq;
+ } else {
+ m->tail->next = &self;
+ m->tail = &self;
+ if (lmtx_seq_lower(lseq, m->firstnum)) m->firstnum = lseq;
+ if (lmtx_seq_higher(lseq, m->lastnum)) m->lastnum = lseq;
+ }
+ if (++m->inqueue == 1) {
+ m->lowseq = m->highseq = lseq;
+ } else {
+ if (lmtx_seq_lower(lseq, m->lowseq)) m->lowseq = lseq;
+ if (lmtx_seq_higher(lseq, m->highseq)) m->highseq = lseq;
+ }
+ lmtx_unlock(&lmtx_locks[bucket]);
+ while (!__atomic_load_n(&self.woken, __ATOMIC_ACQUIRE))
+ LINUX_SYSCALL(__NR_futex, &self.woken, 0 /* FUTEX_WAIT */, 0, 0UL, 0UL, 0);
+ return self.retval & ~LMTX_MTX_WAIT;
+}
long sys_psynch_mutexwait(void* mutex, uint32_t mgen, uint32_t ugen, uint64_t tid, uint32_t flags)
{
+ if (__darling_local_mutex_usable(flags))
+ return __darling_local_mutexwait(mutex, mgen, ugen, flags);
+
uint32_t retval;
+ unsigned long long slowStart = slowwait_enabled() ? slowwait_now() : 0;
int ret = dserver_rpc_psynch_mutexwait(mutex, mgen, ugen, tid, flags, &retval);
+ if (slowStart) {
+ slowwait_log("psynch_mutexwait", (unsigned int)(unsigned long) mutex, slowStart, (void**) __builtin_frame_address(0), flags, 0);
+ }
if (ret < 0) {
if (ret == -LINUX_EINTR) {
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/ulock_wait.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/ulock_wait.c
index f26ec28..2f31473 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/ulock_wait.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/ulock_wait.c
@@ -3,6 +3,7 @@
#include <sys/errno.h>
#include <stdbool.h>
#include <stddef.h>
+#include <darling/emulation/xnu_syscall/bsd/impl/bsdthread/pthread_canceled.h>
#include <darling/emulation/common/base.h>
#include <darling/emulation/conversion/errno.h>
@@ -60,3 +61,42 @@ long sys_ulock_wait(uint32_t operation, void* addr, uint64_t value, uint32_t tim
return ret;
}
+
+/* Nanosecond variant used by libpthread's private condition variables. The
+ * sequence check and sleep are atomic in FUTEX_WAIT; a signal between the
+ * userspace unlock and this call therefore cannot be lost. */
+long sys_ulock_wait2(uint32_t operation, void* addr, uint64_t value,
+ uint64_t timeout, uint64_t value2)
+{
+ const uint32_t allowed = XNU_UL_OPCODE_MASK | XNU_ULF_NO_ERRNO |
+ 0x00010000u /* WORKQ_DATA_CONTENTION */ | 0x00020000u /* CANCEL_POINT */ |
+ 0x00040000u /* ADAPTIVE_SPIN */;
+ int op = operation & XNU_UL_OPCODE_MASK;
+ int ret;
+ if ((operation & ~allowed) || value2 ||
+ (op != XNU_UL_COMPARE_AND_WAIT && op != XNU_UL_UNFAIR_LOCK) ||
+ ((uintptr_t)addr & 3)) {
+ ret = -EINVAL;
+ } else {
+ int cancellation = (operation & 0x00020000u) ? sys_pthread_canceled(0) : -EINVAL;
+ if (cancellation == 0) {
+ ret = -EINTR;
+ } else {
+ /* Darling's server does not implement markcancel/canceled yet.
+ * Bound cancelable sleeps in that case so libpthread can observe
+ * its pending cancellation bit. EINTR is a permitted spurious
+ * condition wake, not a premature timed-wait timeout. */
+ bool cancellation_poll = cancellation == -ENOSYS &&
+ (!timeout || timeout > 50000000ull);
+ uint64_t sleep_ns = cancellation_poll ? 50000000ull : timeout;
+ struct timespec ts = { sleep_ns / 1000000000ull, sleep_ns % 1000000000ull };
+ ret = LINUX_SYSCALL(__NR_futex, addr, FUTEX_WAIT | FUTEX_PRIVATE_FLAG,
+ (uint32_t)value, sleep_ns ? &ts : NULL, 0UL, 0);
+ if (ret == -LINUX_ETIMEDOUT && cancellation_poll) ret = -LINUX_EINTR;
+ if (ret == 0 || ret == -LINUX_EAGAIN) ret = 1;
+ else if (ret < 0) ret = errno_linux_to_bsd(ret);
+ }
+ }
+ if (ret < 0 && (operation & XNU_ULF_NO_ERRNO)) ret &= ~0x800;
+ return ret;
+}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/select/select.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/select/select.c
index c3c33f9..831e8cb 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/select/select.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/select/select.c
@@ -7,6 +7,168 @@
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
#include <darling/emulation/xnu_syscall/bsd/helper/bsdthread/cancelable.h>
+#include <darling/emulation/common/simple.h>
+
+/* darling-lol: opt-in trace for select() busy loops (a thread whose select keeps returning "ready" without
+ * anything consuming the descriptor). Enabled when the host file /tmp/rclol-select-trace exists at the first select
+ * of a process; output is appended to the host /tmp/rclol-select-trace.log. After 5000 consecutive "ready" returns it
+ * logs which descriptors were ready and marks the first readable one; read/recv/recvmsg/close on that descriptor are
+ * then logged too (descriptor numbers and return values only, never data). */
+int rclol_trace_fd = -1;
+static int rclol_trace_state = -1; /* -1 unknown, 0 off, 1 on */
+static unsigned rclol_trace_lines = 0;
+
+static int rclol_trace_enabled(void)
+{
+ if (rclol_trace_state < 0)
+ rclol_trace_state = LINUX_SYSCALL(__NR_access, "/tmp/rclol-select-trace", 0) == 0;
+ return rclol_trace_state;
+}
+
+static void rclol_trace_write(const char* line)
+{
+ unsigned long length = 0;
+ int fd;
+ if (rclol_trace_lines >= 400)
+ return;
+ ++rclol_trace_lines;
+ fd = LINUX_SYSCALL(__NR_open, "/tmp/rclol-select-trace.log", 0x441 /* O_WRONLY | O_CREAT | O_APPEND */, 0644);
+ if (fd < 0)
+ return;
+ while (line[length]) ++length;
+ LINUX_SYSCALL(__NR_write, fd, line, length);
+ LINUX_SYSCALL1(__NR_close, fd);
+}
+
+void rclol_trace_io(const char* op, int fd, long ret)
+{
+ char line[128];
+ if (rclol_trace_state != 1 || fd != rclol_trace_fd || fd < 0)
+ return;
+ __simple_sprintf(line, "io pid=%d tid=%d %s fd=%d ret=%ld\n", (int) LINUX_SYSCALL0(__NR_getpid),
+ (int) LINUX_SYSCALL0(__NR_gettid), op, fd, ret);
+ rclol_trace_write(line);
+}
+
+static void rclol_trace_select(int nfds, const unsigned* rfds, const unsigned* wfds, const unsigned* efds,
+ struct bsd_timeval* timeout, int ret)
+{
+ static unsigned streak = 0;
+ char line[512];
+ int used, fd, listed = 0, first_readable = -1;
+
+ if (ret <= 0) {
+ streak = 0;
+ return;
+ }
+ if (++streak != 5000 && streak % 100000 != 0)
+ return;
+
+ used = __simple_sprintf(line, "select pid=%d tid=%d streak=%u nfds=%d ret=%d timeout=%lds+%dus ready:",
+ (int) LINUX_SYSCALL0(__NR_getpid), (int) LINUX_SYSCALL0(__NR_gettid), streak, nfds, ret,
+ timeout ? (long) timeout->tv_sec : -1L, timeout ? (int) timeout->tv_usec : 0);
+ for (fd = 0; fd < nfds && fd < 1024 && listed < 12; ++fd) {
+ int r = rfds && (rfds[fd / 32] >> (fd % 32) & 1);
+ int w = wfds && (wfds[fd / 32] >> (fd % 32) & 1);
+ int e = efds && (efds[fd / 32] >> (fd % 32) & 1);
+ if (!r && !w && !e)
+ continue;
+ used += __simple_sprintf(line + used, " %d%s%s%s", fd, r ? "r" : "", w ? "w" : "", e ? "e" : "");
+ ++listed;
+ if (r && first_readable < 0)
+ first_readable = fd;
+ }
+ line[used++] = '\n';
+ line[used] = 0;
+ rclol_trace_write(line);
+ if (first_readable >= 0)
+ rclol_trace_fd = first_readable;
+}
+
+/* darling-lol: idle-poll backoff.
+ * The League client's HTTP/TLS reader (LeagueClient thread "ExternalClient") and a second client process call
+ * select() with a zero timeout in a tight loop, 20-30 thousand times a second, finding nothing (measured: 8 million
+ * calls, 99.95% empty, always the same descriptor). That burns ~80% of a core, i.e. several watts of a power-limited
+ * APU that the game's main thread would otherwise turn into clock speed. Limit this workaround to those client
+ * executables: the game also does repeated empty polls on its frame thread, where sleeping delays every frame.
+ * A client thread that has polled with a zero timeout
+ * and found nothing thousands of times in a row is idle-polling, so sleep briefly between its polls: the first
+ * poll that finds something resets the streak, and the added latency is at most 0.5 ms. Streaks are tracked per
+ * thread in a small table. Disable with the host file /tmp/rclol-no-select-backoff. */
+struct rclol_poll_slot { int tid; unsigned streak; };
+static struct rclol_poll_slot rclol_poll_slots[256];
+static int rclol_backoff_state = -1; /* -1 unknown, 0 off, 1 on */
+
+static int rclol_client_poll_backoff_enabled(void)
+{
+ char argv0[512];
+ const char* basename = argv0;
+ const char* clients[] = { "LeagueClient", "RiotClientServices" };
+ long fd, length;
+ unsigned i, j;
+ if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-no-select-backoff", 0) == 0)
+ return 0;
+ /* Native /proc identifies argv[0] without a server RPC or guest path translation.
+ * Fail closed on a read error or a truncated executable name; never log arguments. */
+ fd = LINUX_SYSCALL(__NR_open, "/proc/self/cmdline", 0, 0);
+ if (fd < 0)
+ return 0;
+ length = LINUX_SYSCALL(__NR_read, fd, argv0, sizeof(argv0));
+ LINUX_SYSCALL1(__NR_close, fd);
+ if (length <= 0)
+ return 0;
+ for (i = 0; i < (unsigned) length && argv0[i]; ++i)
+ if (argv0[i] == '/') basename = argv0 + i + 1;
+ if (i == (unsigned) length)
+ return 0;
+ for (i = 0; i < sizeof(clients) / sizeof(clients[0]); ++i) {
+ for (j = 0; basename[j] && basename[j] == clients[i][j]; ++j) {}
+ if (!basename[j] && !clients[i][j])
+ return 1;
+ }
+ return 0;
+}
+
+static void rclol_select_backoff_before(struct bsd_timeval* timeout)
+{
+ struct rclol_poll_slot* slot;
+ unsigned streak;
+ int tid;
+ if (rclol_backoff_state < 0)
+ rclol_backoff_state = rclol_client_poll_backoff_enabled();
+ if (!rclol_backoff_state || timeout == NULL || timeout->tv_sec != 0 || timeout->tv_usec != 0)
+ return;
+ tid = (int) LINUX_SYSCALL0(__NR_gettid);
+ slot = &rclol_poll_slots[(unsigned) tid & 255];
+ if (__atomic_load_n(&slot->tid, __ATOMIC_RELAXED) != tid)
+ return;
+ streak = __atomic_load_n(&slot->streak, __ATOMIC_RELAXED);
+ if (streak >= 2000) {
+ struct { long tv_sec; long tv_nsec; } pause = { 0, streak >= 20000 ? 500000 : 100000 };
+ LINUX_SYSCALL(__NR_nanosleep, &pause, 0);
+ }
+}
+
+static void rclol_select_backoff_after(struct bsd_timeval* timeout, int ret)
+{
+ struct rclol_poll_slot* slot;
+ int tid;
+ if (!rclol_backoff_state)
+ return;
+ tid = (int) LINUX_SYSCALL0(__NR_gettid);
+ slot = &rclol_poll_slots[(unsigned) tid & 255];
+ if (timeout != NULL && timeout->tv_sec == 0 && timeout->tv_usec == 0 && ret == 0) {
+ if (__atomic_load_n(&slot->tid, __ATOMIC_RELAXED) != tid) {
+ __atomic_store_n(&slot->tid, tid, __ATOMIC_RELAXED);
+ __atomic_store_n(&slot->streak, 1, __ATOMIC_RELAXED);
+ } else if (__atomic_load_n(&slot->streak, __ATOMIC_RELAXED) < 1000000) {
+ __atomic_fetch_add(&slot->streak, 1, __ATOMIC_RELAXED);
+ }
+ } else {
+ __atomic_store_n(&slot->streak, 0, __ATOMIC_RELAXED);
+ }
+}
+
long sys_select(int nfds, void* rfds, void* wfds, void* efds, struct bsd_timeval* timeout)
{
CANCELATION_POINT();
@@ -25,6 +187,28 @@ long sys_select_nocancel(int nfds, void* rfds, void* wfds, void* efds, struct bs
ltv.tv_usec = timeout->tv_usec;
}
+ /* every 20000th call of the process: what was asked (taken before the kernel rewrites the sets) */
+ static unsigned rclol_calls = 0, rclol_pos = 0, rclol_zero = 0, rclol_neg = 0;
+ char rclol_line[400];
+ int rclol_used = 0;
+ if (rclol_trace_enabled() && ++rclol_calls % 20000 == 0) {
+ int fd, listed = 0;
+ rclol_used = __simple_sprintf(rclol_line, "sample pid=%d tid=%d calls=%u pos=%u zero=%u neg=%u nfds=%d timeout=%lds+%dus asked:",
+ (int) LINUX_SYSCALL0(__NR_getpid), (int) LINUX_SYSCALL0(__NR_gettid), rclol_calls, rclol_pos, rclol_zero,
+ rclol_neg, nfds, timeout ? (long) timeout->tv_sec : -1L, timeout ? (int) timeout->tv_usec : 0);
+ for (fd = 0; fd < nfds && fd < 1024 && listed < 12; ++fd) {
+ int r = rfds && (((const unsigned*) rfds)[fd / 32] >> (fd % 32) & 1);
+ int w = wfds && (((const unsigned*) wfds)[fd / 32] >> (fd % 32) & 1);
+ int e = efds && (((const unsigned*) efds)[fd / 32] >> (fd % 32) & 1);
+ if (!r && !w && !e)
+ continue;
+ rclol_used += __simple_sprintf(rclol_line + rclol_used, " %d%s%s%s", fd, r ? "r" : "", w ? "w" : "", e ? "e" : "");
+ ++listed;
+ }
+ }
+
+ rclol_select_backoff_before(timeout);
+
#if defined(__NR__newselect)
ret = LINUX_SYSCALL(__NR__newselect, nfds, rfds, wfds, efds,
(timeout != NULL) ? &ltv : NULL);
@@ -32,6 +216,15 @@ long sys_select_nocancel(int nfds, void* rfds, void* wfds, void* efds, struct bs
ret = LINUX_SYSCALL(__NR_select, nfds, rfds, wfds, efds,
(timeout != NULL) ? &ltv : NULL);
#endif
+ rclol_select_backoff_after(timeout, ret);
+ if (rclol_trace_enabled()) {
+ if (ret > 0) ++rclol_pos; else if (ret == 0) ++rclol_zero; else ++rclol_neg;
+ if (rclol_used) {
+ __simple_sprintf(rclol_line + rclol_used, " -> ret=%d\n", ret);
+ rclol_trace_write(rclol_line);
+ }
+ rclol_trace_select(nfds, rfds, wfds, efds, timeout, ret);
+ }
if (ret < 0)
ret = errno_linux_to_bsd(ret);
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/fstatfs.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/fstatfs.c
index 1098510..1741d5a 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/fstatfs.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/fstatfs.c
@@ -19,6 +19,9 @@ extern __SIZE_TYPE__ strlen(const char* str);
extern int strncmp(const char* s1, const char* s2, __SIZE_TYPE__ len);
extern int sprintf(char *str, const char *format, ...);
+/* darling-lol M58F140: shared cached /proc/self/mounts lookup (see statfs.c) */
+VISIBLE void lolsys_fill_mount_names(const char* path, char* mnton, __SIZE_TYPE__ mnton_sz, char* fstype, __SIZE_TYPE__ fstype_sz, char* mntfrom, __SIZE_TYPE__ mntfrom_sz);
+
long sys_fstatfs(int fd, struct bsd_statfs* buf)
{
#ifdef __x86_64__
@@ -31,12 +34,9 @@ long sys_fstatfs(int fd, struct bsd_statfs* buf)
long sys_fstatfs64(int fd, struct bsd_statfs64* buf)
{
- int fd_m, ret;
- struct simple_readline_buf rbuf;
+ int ret;
struct linux_statfs64 lbuf;
- char line[512];
char path[1024];
- int max_len = 0;
#ifdef __NR_fstatfs64
ret = LINUX_SYSCALL3(__NR_fstatfs64, fd, sizeof(lbuf), &lbuf);
@@ -54,48 +54,11 @@ long sys_fstatfs64(int fd, struct bsd_statfs64* buf)
if (ret < 0)
return ret;
- fd_m = sys_open("/proc/self/mounts", O_RDONLY, 0);
- if (fd_m < 0)
- return fd_m;
-
- __simple_readline_init(&rbuf);
-
- buf->f_mntonname[0] = 0;
- buf->f_fstypename[0] = 0;
- buf->f_mntfromname[0] = 0;
buf->f_iosize = 512;
- while (__simple_readline(fd_m, &rbuf, line, sizeof(line)))
- {
- char* p;
- char* saveptr;
- char* mntfrom;
- int len;
-
- p = strtok_r(line, " ", &saveptr);
- if (p == NULL)
- continue;
-
- mntfrom = p;
- p = strtok_r(NULL, " ", &saveptr);
- if (p == NULL)
- continue;
-
- len = strlen(p);
- if (strncmp(p, path, len) != 0 || len < max_len)
- continue;
-
- max_len = len;
- strlcpy(buf->f_mntonname, p, sizeof(buf->f_mntonname));
-
- p = strtok_r(NULL, " ", &saveptr);
- if (p == NULL)
- continue;
-
- strlcpy(buf->f_fstypename, p, sizeof(buf->f_fstypename));
- strlcpy(buf->f_mntfromname, mntfrom, sizeof(buf->f_mntfromname));
- }
-
- close_internal(fd_m);
+ lolsys_fill_mount_names(path,
+ buf->f_mntonname, sizeof(buf->f_mntonname),
+ buf->f_fstypename, sizeof(buf->f_fstypename),
+ buf->f_mntfromname, sizeof(buf->f_mntfromname));
return 0;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/statfs.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/statfs.c
index a6a5162..2155dc4 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/statfs.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/statfs.c
@@ -20,6 +20,92 @@ extern __SIZE_TYPE__ strlcpy(char* dst, const char* src, __SIZE_TYPE__ size);
extern __SIZE_TYPE__ strlen(const char* str);
extern int strncmp(const char* s1, const char* s2, __SIZE_TYPE__ len);
+/* darling-lol M58F140: /proc/self/mounts changes slowly, but statfs/fstatfs
+ * used to re-open and re-parse it on every call. The Riot launcher calls
+ * statfs hundreds of times while scanning its install; the per-call parse was a
+ * large part of its slow start (which tripped the patcher's "no progress"
+ * timeout). Cache the parsed table with a 1-second TTL. */
+#define LOLSYS_MAX_MOUNTS 512
+struct lolsys_mount_entry {
+ char mnton[1024];
+ char fstype[128];
+ char mntfrom[256];
+};
+static struct lolsys_mount_entry lolsys_mount_table[LOLSYS_MAX_MOUNTS];
+static int lolsys_mount_count = 0;
+static long lolsys_mount_sec = -1;
+
+static void lolsys_refresh_mounts(long now_sec)
+{
+ int fd;
+ struct simple_readline_buf rbuf;
+ char line[512];
+
+ if (lolsys_mount_count > 0 && now_sec == lolsys_mount_sec)
+ return;
+
+ lolsys_mount_count = 0;
+ fd = sys_open("/proc/self/mounts", O_RDONLY, 0);
+ if (fd < 0)
+ return;
+
+ __simple_readline_init(&rbuf);
+ while (__simple_readline(fd, &rbuf, line, sizeof(line))) {
+ char* p;
+ char* saveptr;
+ char* mntfrom;
+ struct lolsys_mount_entry* e;
+ if (lolsys_mount_count >= LOLSYS_MAX_MOUNTS)
+ break;
+ p = strtok_r(line, " ", &saveptr);
+ if (p == NULL)
+ continue;
+ mntfrom = p;
+ p = strtok_r(NULL, " ", &saveptr);
+ if (p == NULL)
+ continue;
+ e = &lolsys_mount_table[lolsys_mount_count];
+ strlcpy(e->mnton, p, sizeof(e->mnton));
+ p = strtok_r(NULL, " ", &saveptr);
+ if (p == NULL)
+ continue;
+ strlcpy(e->fstype, p, sizeof(e->fstype));
+ strlcpy(e->mntfrom, mntfrom, sizeof(e->mntfrom));
+ lolsys_mount_count++;
+ }
+ close_internal(fd);
+ lolsys_mount_sec = now_sec;
+}
+
+VISIBLE
+void lolsys_fill_mount_names(const char* path, char* mnton, __SIZE_TYPE__ mnton_sz, char* fstype, __SIZE_TYPE__ fstype_sz, char* mntfrom, __SIZE_TYPE__ mntfrom_sz)
+{
+ int i, max_len = 0;
+ struct { long s; long us; } tv;
+
+ tv.s = 0;
+ tv.us = 0;
+ LINUX_SYSCALL2(__NR_gettimeofday, &tv, 0);
+ lolsys_refresh_mounts(tv.s);
+
+ mnton[0] = 0;
+ fstype[0] = 0;
+ mntfrom[0] = 0;
+
+ for (i = 0; i < lolsys_mount_count; i++) {
+ struct lolsys_mount_entry* e = &lolsys_mount_table[i];
+ int len = (int) strlen(e->mnton);
+ if (len == 0)
+ continue;
+ if (strncmp(e->mnton, path, len) != 0 || len < max_len)
+ continue;
+ max_len = len;
+ strlcpy(mnton, e->mnton, mnton_sz);
+ strlcpy(fstype, e->fstype, fstype_sz);
+ strlcpy(mntfrom, e->mntfrom, mntfrom_sz);
+ }
+}
+
long sys_statfs(const char* path, struct bsd_statfs* buf)
{
#ifdef __x86_64__
@@ -32,11 +118,8 @@ long sys_statfs(const char* path, struct bsd_statfs* buf)
long sys_statfs64(const char* path, struct bsd_statfs64* buf)
{
- int fd, ret;
- struct simple_readline_buf rbuf;
+ int ret;
struct linux_statfs64 lbuf;
- char line[512];
- int max_len = 0;
struct vchroot_expand_args vc;
vc.flags = 0;
@@ -58,47 +141,9 @@ long sys_statfs64(const char* path, struct bsd_statfs64* buf)
statfs_linux_to_bsd64(&lbuf, buf);
- fd = sys_open("/proc/self/mounts", O_RDONLY, 0);
- if (fd < 0)
- return fd;
-
- __simple_readline_init(&rbuf);
-
- buf->f_mntonname[0] = 0;
- buf->f_fstypename[0] = 0;
- buf->f_mntfromname[0] = 0;
-
- while (__simple_readline(fd, &rbuf, line, sizeof(line)))
- {
- char* p;
- char* saveptr;
- char* mntfrom;
- int len;
-
- p = strtok_r(line, " ", &saveptr);
- if (p == NULL)
- continue;
-
- mntfrom = p;
- p = strtok_r(NULL, " ", &saveptr);
- if (p == NULL)
- continue;
-
- len = strlen(p);
- if (strncmp(p, vc.path, len) != 0 || len < max_len)
- continue;
-
- max_len = len;
- strlcpy(buf->f_mntonname, p, sizeof(buf->f_mntonname));
-
- p = strtok_r(NULL, " ", &saveptr);
- if (p == NULL)
- continue;
-
- strlcpy(buf->f_fstypename, p, sizeof(buf->f_fstypename));
- strlcpy(buf->f_mntfromname, mntfrom, sizeof(buf->f_mntfromname));
- }
-
- close_internal(fd);
+ lolsys_fill_mount_names(vc.path,
+ buf->f_mntonname, sizeof(buf->f_mntonname),
+ buf->f_fstypename, sizeof(buf->f_fstypename),
+ buf->f_mntfromname, sizeof(buf->f_mntfromname));
return 0;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/futimes.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/futimes.c
index 687a1b5..83b905a 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/futimes.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/futimes.c
@@ -4,20 +4,25 @@
#include <darling/emulation/common/base.h>
#include <darling/emulation/conversion/errno.h>
+#include <darling/emulation/conversion/duct_errno.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
long sys_futimes(int fd, struct bsd_timeval* tv)
{
int ret;
- struct linux_timeval ltv[2];
+ struct { long tv_sec; long tv_nsec; } times[2];
+ if (tv) {
+ for (int i = 0; i < 2; ++i) {
+ if (tv[i].tv_usec < 0 || tv[i].tv_usec >= 1000000)
+ return errno_linux_to_bsd(-LINUX_EINVAL);
+ times[i].tv_sec = tv[i].tv_sec;
+ times[i].tv_nsec = (long)tv[i].tv_usec * 1000;
+ }
+ }
+ /* NULL requests current timestamps; utimensat uses nanoseconds. */
+ ret = LINUX_SYSCALL(__NR_utimensat, fd, NULL, tv ? times : NULL, 0);
- ltv[0].tv_sec = tv[0].tv_sec;
- ltv[0].tv_usec = tv[0].tv_usec;
- ltv[1].tv_sec = tv[1].tv_sec;
- ltv[1].tv_usec = tv[1].tv_usec;
-
- ret = LINUX_SYSCALL(__NR_utimensat, fd, NULL, ltv, 0);
if (ret < 0)
ret = errno_linux_to_bsd(ret);
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/gettimeofday.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/gettimeofday.c
index be89afd..96bc339 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/gettimeofday.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/gettimeofday.c
@@ -3,12 +3,32 @@
#include <darling/emulation/common/base.h>
#include <darling/emulation/conversion/errno.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
+#include <darling/emulation/linux_premigration/ext/sys/linux_time.h>
long sys_gettimeofday(struct bsd_timeval* tv, struct timezone* tz)
{
int ret;
struct linux_timeval ltv;
+#if defined(__x86_64__)
+ /* darling-lol: user-space clock, see linux_premigration/ext/clock_gettime.c */
+ if (tz == 0)
+ {
+ extern int __darling_vdso_clock_gettime(int clockid, struct timespec* res);
+ extern int __darling_fast_clock(int clockid, struct timespec* res);
+ struct timespec ts;
+ /* The vDSO's CLOCK_REALTIME is the same value the syscall would
+ * return; the TSC path stays the fallback for a host whose vDSO
+ * cannot serve the clock. */
+ if (__darling_vdso_clock_gettime(0 /* CLOCK_REALTIME */, &ts) == 0
+ || __darling_fast_clock(0 /* CLOCK_REALTIME */, &ts) == 0)
+ {
+ tv->tv_sec = ts.tv_sec;
+ tv->tv_usec = ts.tv_nsec / 1000;
+ return 0;
+ }
+ }
+#endif
ret = LINUX_SYSCALL(__NR_gettimeofday, &ltv, tz);
if (ret < 0)
{
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/close.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/close.c
index 6fad87f..e77e786 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/close.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/close.c
@@ -16,6 +16,8 @@ __attribute__((weak))
__attribute__((visibility("default")))
void kqueue_closed_fd(int fd) {}
+extern void rclol_trace_io(const char* op, int fd, long ret);
+
long sys_close(int fd)
{
CANCELATION_POINT();
@@ -39,6 +41,7 @@ long sys_close_nocancel(int fd)
}
ret = LINUX_SYSCALL1(__NR_close, fd);
+ rclol_trace_io("close", fd, ret);
if (ret < 0)
ret = errno_linux_to_bsd(ret);
else
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/read.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/read.c
index bafb315..1e8dd67 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/read.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/read.c
@@ -9,6 +9,9 @@
#define LINUX_TCGETA 0x5405
+extern void rclol_trace_io(const char* op, int fd, long ret);
+
+
long sys_read(int fd, void* mem, int len)
{
CANCELATION_POINT();
@@ -20,6 +23,7 @@ long sys_read_nocancel(int fd, void* mem, int len)
int ret;
ret = LINUX_SYSCALL3(__NR_read, fd, mem, len);
+ rclol_trace_io("read", fd, ret);
if (ret < 0)
{
// When the slave side of a PTY is closed, Linux produces -EIO on the master side.
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fgetxattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fgetxattr.c
index 6dca738..3d864d2 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fgetxattr.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fgetxattr.c
@@ -4,20 +4,23 @@
#include <darling/emulation/common/base.h>
#include <darling/emulation/conversion/errno.h>
+#include <darling/emulation/conversion/xattr/name.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
long sys_fgetxattr(int fd, const char* name, char* value,
unsigned long size, unsigned int pos, int options)
{
int ret;
+ char linux_name[1024];
if (pos != 0)
return -ERANGE;
+ name = xattr_name_to_linux(name, linux_name, sizeof(linux_name));
ret = LINUX_SYSCALL(__NR_fgetxattr, fd, name, value, size);
if (ret < 0)
- return errno_linux_to_bsd(ret);
+ return xattr_errno_to_bsd(ret);
return ret;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fremovexattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fremovexattr.c
index adaff44..eb445fe 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fremovexattr.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fremovexattr.c
@@ -2,16 +2,19 @@
#include <darling/emulation/common/base.h>
#include <darling/emulation/conversion/errno.h>
+#include <darling/emulation/conversion/xattr/name.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
long sys_fremovexattr(int fd, const char* name, int options)
{
int ret;
+ char linux_name[1024];
+ name = xattr_name_to_linux(name, linux_name, sizeof(linux_name));
ret = LINUX_SYSCALL(__NR_fremovexattr, fd, name);
if (ret < 0)
- return errno_linux_to_bsd(ret);
+ return xattr_errno_to_bsd(ret);
return ret;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fsetxattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fsetxattr.c
index 9aaef2c..1770c47 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fsetxattr.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fsetxattr.c
@@ -4,20 +4,23 @@
#include <darling/emulation/common/base.h>
#include <darling/emulation/conversion/errno.h>
+#include <darling/emulation/conversion/xattr/name.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
long sys_fsetxattr(int fd, const char* name, const void* value,
unsigned long size, unsigned int pos, int options)
{
int ret;
+ char linux_name[1024];
if (pos != 0)
return -ERANGE;
+ name = xattr_name_to_linux(name, linux_name, sizeof(linux_name));
ret = LINUX_SYSCALL(__NR_fsetxattr, fd, name, value, size);
if (ret < 0)
- return errno_linux_to_bsd(ret);
+ return xattr_errno_to_bsd(ret);
return ret;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/getxattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/getxattr.c
index daca861..4b53ad2 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/getxattr.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/getxattr.c
@@ -7,6 +7,7 @@ extern char* strcpy(char* dst, const char* src);
#include <darling/emulation/common/base.h>
#include <darling/emulation/conversion/errno.h>
#include <darling/emulation/conversion/xattr/getxattr.h>
+#include <darling/emulation/conversion/xattr/name.h>
#include <darling/emulation/linux_premigration/vchroot_expand.h>
#include <darling/emulation/common/bsdthread/per_thread_wd.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
@@ -15,6 +16,7 @@ long sys_getxattr(const char* path, const char* name, char* value,
unsigned long size, unsigned int pos, int options)
{
int ret;
+ char linux_name[1024];
if (pos != 0)
return -ERANGE;
@@ -29,10 +31,16 @@ long sys_getxattr(const char* path, const char* name, char* value,
if (ret < 0)
return errno_linux_to_bsd(ret);
+ name = xattr_name_to_linux(name, linux_name, sizeof(linux_name));
+
ret = LINUX_SYSCALL(__NR_getxattr, vc.path, name, value, size);
if (ret < 0)
- return errno_linux_to_bsd(ret);
+ return xattr_errno_to_bsd(ret);
- return 0;
+ /* Darling returns the number of bytes copied (POSIX semantics).
+ * Returning 0 here made callers (e.g. Crashpad's
+ * crashpad.database.initialized marker) treat a present attribute as
+ * an empty value, so the crash database never initialised. */
+ return ret;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/listxattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/listxattr.c
index f4740ec..bc88090 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/listxattr.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/listxattr.c
@@ -2,12 +2,38 @@
#include <darling/emulation/common/base.h>
#include <darling/emulation/conversion/errno.h>
+#include <darling/emulation/conversion/xattr/name.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
#include <darling/emulation/linux_premigration/vchroot_expand.h>
#include <darling/emulation/common/bsdthread/per_thread_wd.h>
#define XATTR_NOFOLLOW 1
+/* Strip the "user." prefix from each NUL-separated name in the buffer so
+ * Darwin callers see namespace-less names, and compact the buffer. */
+static unsigned long xattr_strip_user_prefix(char* namebuf, unsigned long len)
+{
+ char* in = namebuf;
+ char* out = namebuf;
+ char* end = namebuf + len;
+
+ while (in < end) {
+ unsigned long n = 0;
+ const char* s = in;
+ char* dst = out;
+ if (__builtin_strncmp(s, "user.", 5) == 0)
+ s += 5;
+ while (s + n < end && s[n] != '\0') {
+ dst[n] = s[n];
+ n++;
+ }
+ dst[n] = '\0';
+ out += n + 1;
+ in += (s - in) + n + 1;
+ }
+ return (unsigned long)(out - namebuf);
+}
+
long sys_listxattr(const char* path, char* namebuf, unsigned long size, int options)
{
int ret;
@@ -23,12 +49,15 @@ long sys_listxattr(const char* path, char* namebuf, unsigned long size, int opti
return errno_linux_to_bsd(ret);
if (options & XATTR_NOFOLLOW)
- ret = LINUX_SYSCALL(__NR_llistxattr, path, namebuf, size);
+ ret = LINUX_SYSCALL(__NR_llistxattr, vc.path, namebuf, size);
else
- ret = LINUX_SYSCALL(__NR_listxattr, path, namebuf, size);
+ ret = LINUX_SYSCALL(__NR_listxattr, vc.path, namebuf, size);
if (ret < 0)
return errno_linux_to_bsd(ret);
+ if (namebuf != 0 && ret > 0)
+ ret = (int) xattr_strip_user_prefix(namebuf, (unsigned long) ret);
+
return ret;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/removexattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/removexattr.c
index 628a4f7..705b845 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/removexattr.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/removexattr.c
@@ -2,6 +2,9 @@
#include <darling/emulation/common/base.h>
#include <darling/emulation/conversion/errno.h>
+#include <darling/emulation/conversion/xattr/name.h>
+#include <darling/emulation/linux_premigration/vchroot_expand.h>
+#include <darling/emulation/common/bsdthread/per_thread_wd.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
#define XATTR_NOFOLLOW 1
@@ -9,14 +12,26 @@
long sys_removexattr(const char* path, const char* name, int options)
{
int ret;
+ char linux_name[1024];
+
+ struct vchroot_expand_args vc;
+ vc.flags = (options & XATTR_NOFOLLOW) ? 0 : VCHROOT_FOLLOW;
+ vc.dfd = get_perthread_wd();
+
+ strcpy(vc.path, path);
+ ret = vchroot_expand(&vc);
+ if (ret < 0)
+ return errno_linux_to_bsd(ret);
+
+ name = xattr_name_to_linux(name, linux_name, sizeof(linux_name));
if (options & XATTR_NOFOLLOW)
- ret = LINUX_SYSCALL(__NR_lremovexattr, path, name);
+ ret = LINUX_SYSCALL(__NR_lremovexattr, vc.path, name);
else
- ret = LINUX_SYSCALL(__NR_removexattr, path, name);
+ ret = LINUX_SYSCALL(__NR_removexattr, vc.path, name);
if (ret < 0)
- return errno_linux_to_bsd(ret);
+ return xattr_errno_to_bsd(ret);
return ret;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/setxattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/setxattr.c
index c1b2d50..4e19963 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/setxattr.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/setxattr.c
@@ -2,8 +2,13 @@
#include <sys/errno.h>
+extern char* strcpy(char* dst, const char* src);
+
#include <darling/emulation/common/base.h>
#include <darling/emulation/conversion/errno.h>
+#include <darling/emulation/conversion/xattr/name.h>
+#include <darling/emulation/linux_premigration/vchroot_expand.h>
+#include <darling/emulation/common/bsdthread/per_thread_wd.h>
#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
#define XATTR_NOFOLLOW 1
@@ -12,21 +17,39 @@ long sys_setxattr(const char* path, const char* name, const void* value,
unsigned long size, unsigned int pos, int options)
{
int ret;
+ char linux_name[1024];
if (pos != 0)
return -ERANGE;
+ /* darling-lol: vchroot-expand the path like getxattr does. Without
+ * this, a guest path (e.g. /Users/.../Crashes/Riot Client) was passed
+ * straight to the host setxattr and failed ENOENT -- Crashpad could
+ * never write its crashpad.database.initialized marker. */
+ struct vchroot_expand_args vc;
+ vc.flags = (options & XATTR_NOFOLLOW) ? 0 : VCHROOT_FOLLOW;
+ vc.dfd = get_perthread_wd();
+
+ strcpy(vc.path, path);
+ ret = vchroot_expand(&vc);
+ if (ret < 0)
+ return errno_linux_to_bsd(ret);
+
+ /* darling-lol: Darwin names are namespace-less; Linux needs a
+ * namespace prefix or returns ENOTSUP. */
+ name = xattr_name_to_linux(name, linux_name, sizeof(linux_name));
+
if (options & XATTR_NOFOLLOW)
{
- ret = LINUX_SYSCALL(__NR_lsetxattr, path, name, value, size);
+ ret = LINUX_SYSCALL(__NR_lsetxattr, vc.path, name, value, size);
}
else
{
- ret = LINUX_SYSCALL(__NR_setxattr, path, name, value, size);
+ ret = LINUX_SYSCALL(__NR_setxattr, vc.path, name, value, size);
}
if (ret < 0)
- return errno_linux_to_bsd(ret);
+ return xattr_errno_to_bsd(ret);
return ret;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/mach/impl/mach_traps.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/mach/impl/mach_traps.c
index b998456..c6281e6 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/mach/impl/mach_traps.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/mach/impl/mach_traps.c
@@ -12,9 +12,28 @@
#include <darling/emulation/linux_premigration/ext/mremap.h>
#include <darling/emulation/common/simple.h>
#include <darling/emulation/conversion/duct_errno.h>
+#include <darling/emulation/linux_premigration/linux-syscalls/linux.h>
#include <darlingserver/rpc.h>
+/* darling-lol: transient darlingserver RPC failures (notably -111/
+ * ECONNREFUSED when many threads check in at once, e.g. Electron
+ * helper bursts) must not kill the process outright. Bounded retry
+ * with backoff; still aborts if the failure persists. */
+struct dserver_retry_ts {
+ long tv_sec;
+ long tv_nsec;
+};
+
+static void dserver_retry_sleep(int attempt)
+{
+ struct dserver_retry_ts ts;
+ long ms = 1L << (attempt > 6 ? 6 : attempt); /* 2..128ms */
+ ts.tv_sec = 0;
+ ts.tv_nsec = ms * 1000L * 1000L;
+ LINUX_SYSCALL(__NR_nanosleep, &ts, 0UL);
+}
+
#define UNIMPLEMENTED_TRAP() { char msg[] = "Called unimplemented Mach trap: "; write(2, msg, sizeof(msg)-1); write(2, __FUNCTION__, sizeof(__FUNCTION__)-1); write(2, "\n", 1); }
mach_port_name_t mach_reply_port_impl(void)
@@ -44,6 +63,8 @@ mach_port_name_t host_self_trap_impl(void)
return port_name;
}
+#include <darling/emulation/common/slowwait.h>
+
mach_msg_return_t mach_msg_trap_impl(
mach_msg_header_t *msg,
mach_msg_option_t option,
@@ -72,8 +93,12 @@ mach_msg_return_t mach_msg_overwrite_trap_impl(
{
int code;
-retry:
+retry:;
+ unsigned long long slowStart = slowwait_enabled() ? slowwait_now() : 0;
code = dserver_rpc_mach_msg_overwrite(msg, option, send_size, rcv_size, rcv_name, timeout, notify, rcv_msg);
+ if (slowStart) {
+ slowwait_log("mach_msg", rcv_name, slowStart, (void**) __builtin_frame_address(0), option, timeout);
+ }
if (code < 0) {
if (code == -LINUX_EINTR) {
@@ -92,33 +117,860 @@ retry:
}
}
__simple_printf("mach_msg_overwrite failed (internally): %d\n", code);
+ {
+ /* darling-lol diagnostic for the LeagueClient crash (2026-10-10): is this thread's RPC descriptor still
+ * open, and what is it? Written to the host file /tmp/rclol-rpc-fail.log as well. */
+ extern int __dserver_per_thread_socket(void);
+ int rpc_fd = __dserver_per_thread_socket();
+ long getfd = LINUX_SYSCALL(__NR_fcntl, rpc_fd, 1 /* F_GETFD */);
+ char link[64], target[128], line[320];
+ __simple_sprintf(link, "/proc/self/fd/%d", rpc_fd);
+ long tl = LINUX_SYSCALL(__NR_readlink, link, target, sizeof(target) - 1);
+ target[tl > 0 ? tl : 0] = 0;
+ int len = __simple_sprintf(line, "LOLRPCFAIL pid=%d tid=%d code=%d rpc_fd=%d getfd=%ld target=%s option=0x%x\n",
+ (int) LINUX_SYSCALL(__NR_getpid), (int) LINUX_SYSCALL(__NR_gettid), code, rpc_fd, getfd, target, (unsigned) option);
+ __simple_printf("%s", line);
+ long log = LINUX_SYSCALL(__NR_open, "/tmp/rclol-rpc-fail.log", 02101, 0666);
+ if (log >= 0) { LINUX_SYSCALL(__NR_write, log, line, len); LINUX_SYSCALL(__NR_close, log); }
+ }
__simple_abort();
}
+ /* A receive can be interrupted after a combined send has completed.
+ * Without MACH_RCV_INTERRUPT, callers expect the receive to resume.
+ * Retrying the send would duplicate the request. */
+ if ((option & MACH_RCV_MSG) != 0 &&
+ code == MACH_RCV_INTERRUPTED &&
+ (option & (MACH_RCV_INTERRUPT | MACH_RCV_TIMEOUT)) == 0) {
+ option &= ~MACH_SEND_MSG;
+ send_size = 0;
+ goto retry;
+ }
+ #if 0 /* M58 diagnostics: keep guest Mach failure traces off by default. */
+ if (code == MACH_SEND_INVALID_DEST && msg != 0 &&
+ (option & MACH_SEND_MSG) != 0) {
+ /* Only failed sends are traced; this is never on the Mach hot path. */
+ __simple_printf("M58GP pid=%d tid=%d send-invalid dest=%u local=%u bits=0x%x id=%d opt=0x%x size=%u\n",
+ (int) LINUX_SYSCALL(__NR_getpid), (int) LINUX_SYSCALL(__NR_gettid),
+ (unsigned) msg->msgh_remote_port,
+ (unsigned) msg->msgh_local_port,
+ (unsigned) msg->msgh_bits,
+ (int) msg->msgh_id,
+ (unsigned) option,
+ (unsigned) send_size);
+ }
+ if (code != MACH_MSG_SUCCESS &&
+ !(code == MACH_RCV_TIMED_OUT && (option & MACH_RCV_TIMEOUT) != 0) &&
+ code != MACH_RCV_TOO_LARGE && (option & MACH_RCV_MSG) != 0) {
+ static unsigned int receive_failures;
+ if (__sync_fetch_and_add(&receive_failures, 1) < 128) {
+ __simple_printf("M58GR pid=%d tid=%d recv-fail code=0x%x rcv=%u opt=0x%x notify=%u send=%u\n",
+ (int) LINUX_SYSCALL(__NR_getpid), (int) LINUX_SYSCALL(__NR_gettid),
+ (unsigned) code, (unsigned) rcv_name,
+ (unsigned) option, (unsigned) notify,
+ (unsigned) send_size);
+ }
+ }
+ #endif
return code;
}
+/* darling-lol: process-local Mach semaphores.
+ *
+ * Every semaphore_wait/semaphore_signal used to be a round trip to darlingserver: the caller sends a message and sleeps
+ * until the server thread has run and answered. League's job system and libdispatch use Mach semaphores to hand work to
+ * threads of the same process (about 19 such round trips per frame on the main thread alone, 2026-10-10), so each
+ * wake-up paid two extra context switches and the caller of semaphore_signal blocked for the server's turnaround.
+ *
+ * A semaphore created by this task (semaphore_create on mach_task_self) is now also kept here as a futex counter and
+ * all traps on it are served locally. The server-side object still exists, so the port name stays valid, but it is
+ * never signalled. Anything the table did not insert itself -- a name it has never seen, a semaphore created by or
+ * for another task, an inherited name in a fork child -- goes to the server path unchanged; the two are never mixed
+ * for one name.
+ *
+ * A slot is (name, generation, value). The generation changes on every assignment and on every destroy, and value+gen
+ * share one atomic word, so a waiter takes a token with a compare-exchange on that word: it can only ever consume a
+ * token of the generation it resolved, and a slot reused after a destroy -- even under the same port name -- is a
+ * different generation, which fails the compare-exchange or one of the generation/name checks every operation makes.
+ * KERN_TERMINATED on a slot that no longer names the resolved object is what Darwin returns for a semaphore that died
+ * while the call was in flight. A fork tombstones the child's copy of the table (see
+ * __darling_local_semaphores_after_fork): the child's IPC space is fresh, no inherited name is valid there.
+ *
+ * LIMITS: a semaphore whose right is sent to another process is signalled through the server on the other side and
+ * never reaches this counter (nothing in the game does this; the counters in build/perf-research/blocking are meant
+ * to prove it before this becomes default); a local wait cannot be interrupted by thread_abort/pthread_cancel (the
+ * server does not know about it), so only a signal, a destroy or its timeout ends one; semaphore_signal_thread with
+ * a specific thread answers KERN_NOT_WAITING. build/perf-research/lsem-fix/REPORT.md has the full list.
+ *
+ * On by default in the League game process only, unless the host file /tmp/rclol-no-local-semaphores exists
+ * (/tmp/rclol-local-semaphores, the former opt-in switch, is now a no-op). While it was on by default in every
+ * process (2026-10-10 22:22-23:50) LeagueClient died ~85 s after start in 3 of 4 checked launches
+ * ("mach_msg_overwrite failed -9", then dserver_rpc_interrupt_enter -9: its RPC descriptor had become invalid) and
+ * the user saw Vanguard errors; that mechanism is not understood, so the clients keep the server path. With the
+ * lifetime fixes above the game ran two full matches with it (2026-10-14: the main thread's server waits fell from
+ * 1.36 to 0.10 ms per frame, 39 to 15 blocks per frame). /tmp/rclol-local-semaphores-all turns it on for every
+ * process (probe tests). Decided once per process and logged
+ * once per game process to /tmp/rclol-game-decisions.log as local_semaphores=0|1. */
+#define LSEM_SLOTS 8192u
+#define LSEM_TOMB 0xffffffffu
+/* What a destroy leaves in the token word: a negative count, so no waiter and no signal can take it for a live
+ * token even if it resolved the slot while the destroy was still in flight (the generation and the name are only
+ * two more loads away from being visible; the value is the one the token compare-exchange goes through). */
+#define LSEM_TOMB_VALUE (-1)
+#define LSEM_FUTEX_WAIT 0
+#define LSEM_FUTEX_WAKE 1
+#define LSEM_FUTEX_PRIVATE 0x80u /* FUTEX_PRIVATE_FLAG: the word is always this process's own memory */
+/* The token count and the generation are one atomic word: lsem_wait takes a token with a compare-exchange on the
+ * pair, so the token can only come from the generation it checked. The low half is the futex word the syscalls
+ * address; every value write is a generation-checked compare-exchange, never a plain add. */
+union lsem_pv {
+ unsigned long long pair;
+ struct {
+ volatile int value;
+ volatile unsigned int gen;
+ } w;
+};
+_Static_assert(sizeof(union lsem_pv) == 8 && _Alignof(union lsem_pv) == 8,
+ "the token and the generation must share one naturally aligned atomic word");
+struct lsem {
+ union lsem_pv pv;
+ volatile unsigned int name;
+ volatile int waiters; /* a hint for the signal wake filter and signal_all's count; the futex protocol carries correctness */
+};
+/* The second semaphore of a combined wait_signal/timedwait_signal trap. XNU registers the wait on the wait semaphore
+ * and only then signals the signal semaphore (even when the wait did not block); a local wait signals it from inside
+ * the loop, at that same point (see lsem_post_signal). */
+struct lsem_post {
+ struct lsem* s; /* 0: not local, signal the name through the server */
+ unsigned int name;
+ unsigned int gen;
+ volatile int done;
+};
+static struct lsem lsems[LSEM_SLOTS];
+static volatile int lsem_state; /* 0 undecided, 1 on, -1 off */
+static volatile int lsem_lock;
+
+extern kern_return_t __darling_rpc_semaphore_create(mach_port_name_t task, mach_port_name_t* semaphore, int policy, int value);
+extern kern_return_t __darling_rpc_semaphore_destroy(mach_port_name_t task, mach_port_name_t semaphore);
+
+static int darling_is_league_game_uncached(void);
+
+/* Read once per process: the answer cannot change, and other processes must not keep opening files for it. */
+static int darling_is_league_game(void)
+{
+ static volatile int cached; /* 0 unknown, 1 yes, -1 no */
+ int value = __atomic_load_n(&cached, __ATOMIC_ACQUIRE);
+ if (value == 0) {
+ value = darling_is_league_game_uncached() ? 1 : -1;
+ __atomic_store_n(&cached, value, __ATOMIC_RELEASE);
+ }
+ return value == 1;
+}
+
+static int darling_is_league_game_uncached(void)
+{
+ /* Only the executable path (argv[0], the first NUL-terminated string) counts. The whole command line was searched
+ * before, so any process that merely had the game's path among its arguments would have been treated as the game. */
+ char buf[512];
+ static const char mark[] = "/Game/LeagueOfLegends.app/Contents/MacOS/";
+ int found = 0;
+ long n = 0;
+ int fd = LINUX_SYSCALL(__NR_open, "/proc/self/cmdline", 0 /* O_RDONLY */, 0);
+ if (fd >= 0) {
+ n = LINUX_SYSCALL(__NR_read, fd, buf, sizeof(buf) - 1);
+ LINUX_SYSCALL(__NR_close, fd);
+ if (n < 0) n = 0;
+ buf[n] = 0;
+ long end = 0;
+ while (end < n && buf[end]) ++end; /* argv[0] */
+ for (long i = 0; i + (long) sizeof(mark) - 1 <= end && !found; ++i) {
+ unsigned long j = 0;
+ while (j < sizeof(mark) - 1 && buf[i + j] == mark[j]) ++j;
+ if (j == sizeof(mark) - 1) found = 1;
+ }
+ /* one line per process in the host file /tmp/rclol-game-decisions.log: which processes asked, and the answer */
+ long log = LINUX_SYSCALL(__NR_open, "/tmp/rclol-game-decisions.log", 02101 /* O_WRONLY|O_CREAT|O_APPEND */, 0666);
+ if (log >= 0) {
+ char line[160];
+ long tail = end > 60 ? end - 60 : 0;
+ int len = __simple_sprintf(line, "pid=%d game=%d argv0=...%s\n", (int) LINUX_SYSCALL(__NR_getpid), found, buf + tail);
+ LINUX_SYSCALL(__NR_write, log, line, len);
+ LINUX_SYSCALL(__NR_close, log);
+ }
+ }
+ return found;
+}
+
+/* for the other emulation files (psynch) */
+__attribute__((visibility("hidden")))
+int __darling_is_league_game(void)
+{
+ return darling_is_league_game();
+}
+
+/* darling-lol: core isolation for the game's initial thread.
+ *
+ * The game's main thread is the bottleneck (~80% of one core) and in the last matches 16% of its running time (up
+ * to 36% in fights) another thread of ours ran on its SMT sibling. Linux core scheduling (CONFIG_SCHED_CORE,
+ * prctl PR_SCHED_CORE, 5.13+) lets a task carry a cookie: tasks with different cookies (an untagged task has
+ * cookie 0) are never scheduled on the same physical core at the same time, the losing sibling is forced idle.
+ * The main thread creates a cookie of its own, so no other thread of the process, no darlingserver thread and no
+ * other process can share its core; the kernel keeps the rest of the system's SMT pairing as it was. On this host
+ * (first run 2026-10-14, build/perf-research/core-isolation/) two threads pinned to the two SMT siblings of one
+ * core did one thread's work when one was tagged and the other was not, and twice that when both were untagged or
+ * shared a cookie.
+ *
+ * A new thread inherits the cookie of the thread that created it (kernel core_sched.c: sched_core_fork()), so a
+ * thread created by the tagged main thread would carry the main cookie and could run on its core again. Every
+ * other thread of the process therefore shares one *other* cookie instead: the first one to start creates it and
+ * remembers its tid, later threads take it over with PR_SCHED_CORE_SHARE_FROM. They stay in one group (they may
+ * still share cores with each other, as before) and only the main thread becomes incompatible with everything
+ * else. Thread pools retire their threads, so if the recorded owner is gone (SHARE_FROM fails) the caller creates
+ * a fresh cookie and takes the role over. A forked child inherits the record as well, and the tid in it can then
+ * name a task of the parent (or of an unrelated process), so the record is only used while it belongs to the
+ * process reading it; a child's first thread takes the worker role over with a cookie of its own.
+ *
+ * Best effort throughout: if the prctl is unavailable (EINVAL/EPERM/ENODEV - host without CONFIG_SCHED_CORE, SMT
+ * off, or a kernel without the feature) nothing changes.
+ *
+ * Opt-in, only with the host file /tmp/rclol-core-isolation: in the first match with it on (2026-10-14, Hexgate,
+ * same minutes compared) the game fell from 118 to 103 fps. The main thread waited for a CPU 12.6% of the time
+ * (1% without) and the other game threads queued as well: forced idle siblings cost more on this busy 6-core host
+ * than the sharing they prevent.
+ * The decision is appended once per process to /tmp/rclol-game-decisions.log as
+ * "core_isolation=0|1 errno=<prctl result, -1 off, -2 registration was not on the initial thread> cookie=<hash>". */
+#define LCORE_PRCTL_SCHED_CORE 62 /* include/uapi/linux/prctl.h */
+#define LCORE_GET 0
+#define LCORE_CREATE 1 /* give the calling thread a cookie of its own */
+#define LCORE_SHARE_FROM 3 /* the calling thread takes over the cookie of the thread at arg3 */
+#define LCORE_SCOPE_THREAD 0
+
+static volatile int lcore_state; /* 0 undecided, 1 on, -1 off */
+static volatile int lcore_main; /* 0 not tried, 1 the initial thread has its own cookie, -1 it has none */
+static volatile int lcore_worker_tid; /* tid whose cookie the other threads share, 0 = none yet */
+static volatile int lcore_worker_pid; /* process that tid belongs to; a forked child must not use it */
+
+static void lcore_log(int on, long err, unsigned long long cookie)
+{
+ char line[128];
+ long log = LINUX_SYSCALL(__NR_open, "/tmp/rclol-game-decisions.log", 02101 /* O_WRONLY|O_CREAT|O_APPEND */, 0666);
+ if (log < 0)
+ return;
+ int len = __simple_sprintf(line, "pid=%d core_isolation=%d errno=%ld cookie=%llu\n",
+ (int) LINUX_SYSCALL(__NR_getpid), on, err, cookie);
+ LINUX_SYSCALL(__NR_write, log, line, len);
+ LINUX_SYSCALL(__NR_close, log);
+}
+
+static int lcore_decide(void)
+{
+ int state = __atomic_load_n(&lcore_state, __ATOMIC_ACQUIRE);
+ if (state != 0)
+ return state;
+ if (__darling_is_league_game() && LINUX_SYSCALL(__NR_access, "/tmp/rclol-core-isolation", 0) == 0)
+ state = 1;
+ else
+ state = -1;
+ __atomic_store_n(&lcore_state, state, __ATOMIC_RELEASE);
+ return state;
+}
+
+/* Called from sys_bsdthread_register (libpthread's per-process initialization). */
+__attribute__((visibility("hidden")))
+void __darling_core_isolate_main_thread(void)
+{
+ unsigned long long cookie = 0;
+ int expected = 0;
+ long err;
+
+ /* once per process: a forked child keeps (inherits) whatever the parent's initial thread had */
+ if (!__atomic_compare_exchange_n(&lcore_main, &expected, -1, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE))
+ return;
+
+ if (lcore_decide() != 1) {
+ /* only the game logs its decision; every other Darling process would add a line otherwise */
+ if (__darling_is_league_game())
+ lcore_log(0, -1, 0); /* /tmp/rclol-core-isolation absent */
+ return;
+ }
+ /* Only the process' initial thread may take this cookie: its threads are created with clone() from it and
+ * the thread entry clears the inherited cookie again. gettid() == getpid() is that initial thread. If
+ * registration ever ran on another thread, leave everything untagged rather than tag the wrong thread. */
+ if (LINUX_SYSCALL(__NR_gettid) != LINUX_SYSCALL(__NR_getpid)) {
+ lcore_log(0, -2, 0);
+ return;
+ }
+ err = LINUX_SYSCALL5(__NR_prctl, LCORE_PRCTL_SCHED_CORE, LCORE_CREATE, 0, LCORE_SCOPE_THREAD, 0);
+ if (err == 0) {
+ long got = LINUX_SYSCALL5(__NR_prctl, LCORE_PRCTL_SCHED_CORE, LCORE_GET, 0, LCORE_SCOPE_THREAD, &cookie);
+ if (got != 0)
+ cookie = 0;
+ __atomic_store_n(&lcore_main, 1, __ATOMIC_RELEASE);
+ }
+ lcore_log(err == 0, err, cookie);
+}
+
+/* Called from the entry of every new guest thread (pthread_entry_point_wrapper and
+ * wqueue_entry_point_wrapper). */
+__attribute__((visibility("hidden")))
+void __darling_core_isolate_new_thread(void)
+{
+ int pid, me, owner = 0;
+ int expected;
+
+ /* the initial thread was not tagged (not the game, opted out, or the prctl failed): nothing to avoid */
+ if (__atomic_load_n(&lcore_main, __ATOMIC_ACQUIRE) != 1)
+ return;
+
+ pid = (int) LINUX_SYSCALL(__NR_getpid);
+ me = (int) LINUX_SYSCALL(__NR_gettid);
+ if (me == pid)
+ return; /* the initial thread keeps its own cookie */
+
+ if (__atomic_load_n(&lcore_worker_pid, __ATOMIC_ACQUIRE) == pid)
+ owner = __atomic_load_n(&lcore_worker_tid, __ATOMIC_ACQUIRE);
+
+ if (owner == 0) {
+ if (LINUX_SYSCALL5(__NR_prctl, LCORE_PRCTL_SCHED_CORE, LCORE_CREATE, 0, LCORE_SCOPE_THREAD, 0) != 0)
+ return; /* no cookie for this thread; it keeps the one it inherited */
+ expected = 0;
+ if (__atomic_compare_exchange_n(&lcore_worker_tid, &expected, me, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
+ __atomic_store_n(&lcore_worker_pid, pid, __ATOMIC_RELEASE);
+ return; /* this thread owns the shared worker cookie from now on */
+ }
+ /* the published owner belongs to a process that no longer is this one (a forked child saw the
+ * parent's record): it must not be used, the cookie created above becomes the shared one */
+ if (__atomic_load_n(&lcore_worker_pid, __ATOMIC_ACQUIRE) != pid) {
+ __atomic_store_n(&lcore_worker_tid, me, __ATOMIC_RELEASE);
+ __atomic_store_n(&lcore_worker_pid, pid, __ATOMIC_RELEASE);
+ return;
+ }
+ owner = expected; /* another thread of this process published its cookie first: join it */
+ }
+
+ if (owner == me)
+ return;
+ if (LINUX_SYSCALL5(__NR_prctl, LCORE_PRCTL_SCHED_CORE, LCORE_SHARE_FROM, owner, LCORE_SCOPE_THREAD, 0) == 0)
+ return;
+ /* the recorded owner is gone (retired worker, or a parent process after fork): take the role over */
+ if (LINUX_SYSCALL5(__NR_prctl, LCORE_PRCTL_SCHED_CORE, LCORE_CREATE, 0, LCORE_SCOPE_THREAD, 0) != 0)
+ return;
+ expected = owner;
+ if (__atomic_compare_exchange_n(&lcore_worker_tid, &expected, me, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
+ __atomic_store_n(&lcore_worker_pid, pid, __ATOMIC_RELEASE);
+ return;
+ }
+ if (expected != 0 && expected != me)
+ LINUX_SYSCALL5(__NR_prctl, LCORE_PRCTL_SCHED_CORE, LCORE_SHARE_FROM, expected, LCORE_SCOPE_THREAD, 0);
+}
+
+static void lsem_log(int on)
+{
+ char line[96];
+ long log = LINUX_SYSCALL(__NR_open, "/tmp/rclol-game-decisions.log", 02101 /* O_WRONLY|O_CREAT|O_APPEND */, 0666);
+ if (log < 0)
+ return;
+ int len = __simple_sprintf(line, "pid=%d local_semaphores=%d\n", (int) LINUX_SYSCALL(__NR_getpid), on);
+ LINUX_SYSCALL(__NR_write, log, line, len);
+ LINUX_SYSCALL(__NR_close, log);
+}
+
+static int lsem_decide(void)
+{
+ int state = __atomic_load_n(&lsem_state, __ATOMIC_ACQUIRE);
+ int on = 0;
+ int expected = 0;
+ if (state != 0)
+ return state;
+ if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-local-semaphores-all", 0) == 0)
+ on = 1;
+ else if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-no-local-semaphores", 0) != 0)
+ on = darling_is_league_game();
+ if (__atomic_compare_exchange_n(&lsem_state, &expected, on ? 1 : -1, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
+ /* the one line per game process in the host file, next to core_isolation (lcore_log) */
+ if (darling_is_league_game())
+ lsem_log(on);
+ return on ? 1 : -1;
+ }
+ return __atomic_load_n(&lsem_state, __ATOMIC_ACQUIRE); /* another thread decided first */
+}
+
+static inline unsigned int lsem_hash(unsigned int name)
+{
+ return (name * 2654435761u) >> 19; /* 13 bits */
+}
+
+/* The generation the caller's lsem_find resolved. Every operation re-checks it, so work that started before a
+ * destroy can never land on the object that took the slot afterwards. */
+static inline unsigned int lsem_capture(struct lsem* s)
+{
+ return __atomic_load_n(&s->pv.w.gen, __ATOMIC_ACQUIRE);
+}
+
+static struct lsem* lsem_find(unsigned int name)
+{
+ if (__atomic_load_n(&lsem_state, __ATOMIC_ACQUIRE) != 1 || name == 0 || name == LSEM_TOMB)
+ return 0;
+ unsigned int i = lsem_hash(name);
+ for (unsigned int n = 0; n < LSEM_SLOTS; ++n, i = (i + 1) & (LSEM_SLOTS - 1)) {
+ unsigned int have = __atomic_load_n(&lsems[i].name, __ATOMIC_ACQUIRE);
+ if (have == name)
+ return &lsems[i];
+ if (have == 0)
+ return 0;
+ }
+ return 0;
+}
+
+static void lsem_table_lock(void)
+{
+ int expected = 0;
+ while (!__atomic_compare_exchange_n(&lsem_lock, &expected, 1, 0, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) {
+ LINUX_SYSCALL(__NR_futex, &lsem_lock, LSEM_FUTEX_WAIT | LSEM_FUTEX_PRIVATE, 1, 0UL, 0UL, 0);
+ expected = 0;
+ }
+}
+
+static void lsem_table_unlock(void)
+{
+ __atomic_store_n(&lsem_lock, 0, __ATOMIC_RELEASE);
+ LINUX_SYSCALL(__NR_futex, &lsem_lock, LSEM_FUTEX_WAKE | LSEM_FUTEX_PRIVATE, 1, 0UL, 0UL, 0);
+}
+
+static void lsem_insert(unsigned int name, int value)
+{
+ if (name == 0 || name == LSEM_TOMB)
+ return;
+ lsem_table_lock();
+ struct lsem* free_slot = 0;
+ struct lsem* found = 0;
+ unsigned int i = lsem_hash(name);
+ for (unsigned int n = 0; n < LSEM_SLOTS; ++n, i = (i + 1) & (LSEM_SLOTS - 1)) {
+ unsigned int have = lsems[i].name;
+ if (have == name) { found = &lsems[i]; break; }
+ if (have == LSEM_TOMB && !free_slot) free_slot = &lsems[i];
+ if (have == 0) { if (!free_slot) free_slot = &lsems[i]; break; }
+ }
+ if (!found) found = free_slot;
+ if (found) { /* a full table leaves the semaphore to the server */
+ /* The new generation and the initial value are one atomic word: a waiter that looked up the old
+ * generation can never observe a half-updated slot, and no generation is ever handed out twice. */
+ unsigned long long old = __atomic_load_n(&found->pv.pair, __ATOMIC_RELAXED);
+ unsigned long long fresh = (((old >> 32) + 1) << 32) | (unsigned int) (value < 0 ? 0 : value);
+ __atomic_store_n(&found->pv.pair, fresh, __ATOMIC_RELEASE);
+ __atomic_store_n(&found->waiters, 0, __ATOMIC_RELAXED);
+ __atomic_store_n(&found->name, name, __ATOMIC_RELEASE);
+ }
+ lsem_table_unlock();
+}
+
+static void lsem_remove(unsigned int name)
+{
+ struct lsem* s = lsem_find(name);
+ if (!s)
+ return;
+ lsem_table_lock();
+ /* Between the find and the lock another thread may have destroyed (and re-created) the name: the slot then
+ * holds an object this destroy was not called on, so leave it alone. */
+ if (__atomic_load_n(&s->name, __ATOMIC_ACQUIRE) != name) {
+ lsem_table_unlock();
+ return;
+ }
+ /* gen+1 and the tombstone value in one word: a waiter either took its token before this compare-exchange or it
+ * sees the new generation (or the negative value) and returns KERN_TERMINATED; a waiter of a later object in
+ * the slot can never see a generation it did not resolve. */
+ for (;;) {
+ unsigned long long old = __atomic_load_n(&s->pv.pair, __ATOMIC_SEQ_CST);
+ unsigned long long want = (((old >> 32) + 1) << 32) | (unsigned int) LSEM_TOMB_VALUE;
+ if (__atomic_compare_exchange_n(&s->pv.pair, &old, want, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST))
+ break;
+ }
+ __atomic_store_n(&s->name, LSEM_TOMB, __ATOMIC_RELEASE);
+ __atomic_store_n(&s->waiters, 0, __ATOMIC_RELAXED);
+ LINUX_SYSCALL(__NR_futex, &s->pv.w.value, LSEM_FUTEX_WAKE | LSEM_FUTEX_PRIVATE, 0x7fffffff, 0UL, 0UL, 0);
+ lsem_table_unlock();
+}
+
+/* Returns KERN_TERMINATED when the slot no longer names the object the caller resolved: the check and the token are
+ * one compare-exchange on the generation+value word, so no token is ever added to the object that took the slot. */
+static kern_return_t lsem_signal(struct lsem* s, unsigned int name, unsigned int gen)
+{
+ for (;;) {
+ unsigned long long old = __atomic_load_n(&s->pv.pair, __ATOMIC_SEQ_CST);
+ if ((unsigned int) (old >> 32) != gen || __atomic_load_n(&s->name, __ATOMIC_ACQUIRE) != name)
+ return KERN_TERMINATED;
+ if ((int) (unsigned int) old < 0)
+ return KERN_TERMINATED; /* the tombstone value: the object is being destroyed */
+ if (__atomic_compare_exchange_n(&s->pv.pair, &old, old + 1, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST))
+ break;
+ }
+ if (__atomic_load_n(&s->waiters, __ATOMIC_SEQ_CST) > 0)
+ LINUX_SYSCALL(__NR_futex, &s->pv.w.value, LSEM_FUTEX_WAKE | LSEM_FUTEX_PRIVATE, 1, 0UL, 0UL, 0);
+ return KERN_SUCCESS;
+}
+
+static kern_return_t lsem_signal_all(struct lsem* s, unsigned int name, unsigned int gen)
+{
+ for (;;) {
+ unsigned long long old = __atomic_load_n(&s->pv.pair, __ATOMIC_SEQ_CST);
+ int n;
+ if ((unsigned int) (old >> 32) != gen || __atomic_load_n(&s->name, __ATOMIC_ACQUIRE) != name)
+ return KERN_TERMINATED;
+ if ((int) (unsigned int) old < 0)
+ return KERN_TERMINATED; /* the tombstone value: the object is being destroyed */
+ /* wake everybody who is waiting now; nothing is left over for later waiters (XNU resets the count, and
+ * the trap version banks no count when nobody waits) */
+ n = __atomic_load_n(&s->waiters, __ATOMIC_SEQ_CST);
+ if (n <= 0)
+ return KERN_SUCCESS;
+ if (__atomic_compare_exchange_n(&s->pv.pair, &old, old + (unsigned int) n, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) {
+ LINUX_SYSCALL(__NR_futex, &s->pv.w.value, LSEM_FUTEX_WAKE | LSEM_FUTEX_PRIVATE, n, 0UL, 0UL, 0);
+ return KERN_SUCCESS;
+ }
+ }
+}
+
+static kern_return_t lsem_signal_thread(struct lsem* s, unsigned int name, unsigned int gen, unsigned int thread_name)
+{
+ unsigned long long old = __atomic_load_n(&s->pv.pair, __ATOMIC_SEQ_CST);
+ if ((unsigned int) (old >> 32) != gen || __atomic_load_n(&s->name, __ATOMIC_ACQUIRE) != name ||
+ (int) (unsigned int) old < 0)
+ return KERN_TERMINATED;
+ if (thread_name != 0)
+ return KERN_NOT_WAITING; /* a specific thread is not addressable here; see the header comment */
+ if (__atomic_load_n(&s->waiters, __ATOMIC_SEQ_CST) <= 0)
+ return KERN_NOT_WAITING; /* nobody waiting: XNU banks no count here either */
+ return lsem_signal(s, name, gen);
+}
+
+/* Signal the signal semaphore of a combined trap, once, at the point XNU would (after the wait on the other
+ * semaphore registered itself, or after it already succeeded). */
+static kern_return_t lsem_post_signal(struct lsem_post* post)
+{
+ if (!post || __atomic_exchange_n(&post->done, 1, __ATOMIC_ACQ_REL))
+ return KERN_SUCCESS;
+ if (post->s)
+ return lsem_signal(post->s, post->name, post->gen);
+ return semaphore_signal_trap_impl(post->name); /* not local: signal it through the server */
+}
+
+/* timeout: nanoseconds, or -1 to wait forever. (name, gen) is what the caller's lsem_find resolved. */
+static kern_return_t lsem_wait(struct lsem* s, unsigned int name, unsigned int gen, long long timeout_ns, struct lsem_post* post)
+{
+ struct { long tv_sec; long tv_nsec; } now, rel;
+ long long deadline = 0;
+ if (timeout_ns >= 0) {
+ LINUX_SYSCALL(__NR_clock_gettime, 1 /* CLOCK_MONOTONIC */, &now);
+ deadline = now.tv_sec * 1000000000ll + now.tv_nsec + timeout_ns;
+ }
+ for (;;) {
+ /* both the generation and the name have to match: a slot reused for a different object, even one that
+ * got the same port name again, must not be waited on (Darwin terminated the object we resolved) */
+ if ((unsigned int) __atomic_load_n(&s->pv.w.gen, __ATOMIC_ACQUIRE) != gen ||
+ __atomic_load_n(&s->name, __ATOMIC_ACQUIRE) != name) {
+ (void) lsem_post_signal(post); /* XNU signals the signal semaphore even when the wait fails */
+ return KERN_TERMINATED;
+ }
+ unsigned long long old = __atomic_load_n(&s->pv.pair, __ATOMIC_SEQ_CST);
+ if ((unsigned int) (old >> 32) != gen)
+ continue; /* the slot changed while we looked at it: the top of the loop reports it */
+ int v = (int) (unsigned int) old;
+ if (v < 0) {
+ /* the destroy tombstone: a trap that resolved the slot while the destroy was in flight must
+ * report the death, never take the tombstone for a token */
+ (void) lsem_post_signal(post);
+ return KERN_TERMINATED;
+ }
+ if (v > 0) {
+ /* take the token only if value and generation still belong to this object: the compare-exchange
+ * is on the whole pair, so a reused slot cannot hand its token to this waiter */
+ if (__atomic_compare_exchange_n(&s->pv.pair, &old, old - 1, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) {
+ kern_return_t kr = lsem_post_signal(post);
+ /* the wait succeeded; XNU swallows a dead signal semaphore in this case */
+ return (kr == KERN_SUCCESS || kr == KERN_TERMINATED) ? KERN_SUCCESS : kr;
+ }
+ continue;
+ }
+ void* ts = 0;
+ if (timeout_ns >= 0) {
+ LINUX_SYSCALL(__NR_clock_gettime, 1, &now);
+ long long left = deadline - (now.tv_sec * 1000000000ll + now.tv_nsec);
+ if (left <= 0) {
+ kern_return_t kr = lsem_post_signal(post);
+ return (kr == KERN_SUCCESS || kr == KERN_TERMINATED) ? KERN_OPERATION_TIMED_OUT : kr;
+ }
+ rel.tv_sec = left / 1000000000ll;
+ rel.tv_nsec = left % 1000000000ll;
+ ts = &rel;
+ }
+ /* register (signal_all and signal_thread see us) and only then release the signal semaphore, as XNU does */
+ __atomic_add_fetch(&s->waiters, 1, __ATOMIC_SEQ_CST);
+ if (post) {
+ kern_return_t kr = lsem_post_signal(post);
+ if (kr != KERN_SUCCESS) {
+ __atomic_sub_fetch(&s->waiters, 1, __ATOMIC_SEQ_CST);
+ return kr;
+ }
+ }
+ LINUX_SYSCALL(__NR_futex, &s->pv.w.value, LSEM_FUTEX_WAIT | LSEM_FUTEX_PRIVATE, 0, ts, 0UL, 0);
+ __atomic_sub_fetch(&s->waiters, 1, __ATOMIC_SEQ_CST);
+ }
+}
+
+VISIBLE
+kern_return_t semaphore_create(mach_port_name_t task, mach_port_name_t* semaphore, int policy, int value)
+{
+ kern_return_t kr = __darling_rpc_semaphore_create(task, semaphore, policy, value);
+ if (kr == KERN_SUCCESS && task == mach_task_self()) {
+ int state = __atomic_load_n(&lsem_state, __ATOMIC_ACQUIRE);
+ if (state == 0)
+ state = lsem_decide();
+ if (state == 1)
+ lsem_insert(*semaphore, value);
+ }
+ return kr;
+}
+
+VISIBLE
+kern_return_t semaphore_destroy(mach_port_name_t task, mach_port_name_t semaphore)
+{
+ if (task == mach_task_self())
+ lsem_remove(semaphore);
+ return __darling_rpc_semaphore_destroy(task, semaphore);
+}
+
+/* darling-lol: the child of fork() has a fresh IPC space (duct-tape ipc_task_init creates an empty one), so a
+ * semaphore name inherited from the parent is not valid in the child. Without this reset the inherited table would
+ * serve the child's traps for those names locally: a wait would succeed without any object behind it (or block where
+ * the server would answer KERN_INVALID_NAME), and a signal would be a no-op the parent (a different address space
+ * after fork) never sees. Runs in the forking thread right after the host fork, before any other thread of the child
+ * exists: the table has no waiters to wake, and the lock (possibly held by a parent thread that does not exist in the
+ * child) can simply be dropped. */
+__attribute__((visibility("hidden")))
+void __darling_local_semaphores_after_fork(void)
+{
+ if (__atomic_load_n(&lsem_state, __ATOMIC_ACQUIRE) != 1)
+ return;
+ for (unsigned int i = 0; i < LSEM_SLOTS; ++i) {
+ unsigned int have = lsems[i].name;
+ if (have != 0 && have != LSEM_TOMB) {
+ unsigned long long old = __atomic_load_n(&lsems[i].pv.pair, __ATOMIC_RELAXED);
+ __atomic_store_n(&lsems[i].pv.pair, (((old >> 32) + 1) << 32) | 0, __ATOMIC_RELEASE);
+ __atomic_store_n(&lsems[i].name, LSEM_TOMB, __ATOMIC_RELEASE);
+ __atomic_store_n(&lsems[i].waiters, 0, __ATOMIC_RELAXED);
+ }
+ }
+ lsem_lock = 0;
+}
+
+/* darling-lol: mach_vm_region_recurse on the caller's own task, answered in the process.
+ *
+ * darlingserver answers this call from the kernel's PROCMAP_QUERY ioctl on /proc/<pid>/maps (duct-tape memory.c
+ * mach_vm_region_recurse + Process::memoryRegionInfo/getNextRegion). In a League match one thread asks ~9,000 times a
+ * second, which kept two server threads at 60% each and made every other RPC of the game queue behind it (server
+ * 120% CPU in a fresh match, 2026-10-10). The same ioctl on /proc/self/maps returns the same record without the round
+ * trip; the reply is built exactly as the server builds it. Other tasks and failures of the ioctl still go to the server.
+ * Game process only; host file /tmp/rclol-no-local-region turns it off, /tmp/rclol-local-region-all enables it for
+ * every process (probe tests). Refreshed every 1024 queries for live A/B. */
+struct lregion_query {
+ unsigned long long size, query_flags, query_addr, vma_start, vma_end, vma_flags, vma_page_size, vma_offset, inode;
+ unsigned int dev_major, dev_minor, vma_name_size, build_id_size;
+ unsigned long long vma_name_addr, build_id_addr;
+};
+#define LREGION_IOCTL 0xC0686611ul /* PROCMAP_QUERY = _IOWR('f', 17, struct procmap_query), 104 bytes */
+#define LREGION_SHORT_INFO_COUNT 12 /* VM_REGION_SUBMAP_SHORT_INFO_COUNT_64 */
+struct lregion_short_info {
+ int protection, max_protection;
+ unsigned int inheritance;
+ unsigned long long offset;
+ unsigned int user_tag, ref_count;
+ unsigned short shadow_depth;
+ unsigned char external_pager, share_mode;
+ int is_submap, behavior;
+ unsigned int object_id;
+ unsigned short user_wired_count;
+} __attribute__((packed, aligned(4)));
+static volatile int lregion_state; /* 0 undecided, 1 on, -1 off */
+static unsigned int lregion_calls;
+static volatile int lregion_refresh;
+static volatile int lregion_maps_fd = -1;
+static volatile int lregion_maps_pid;
+_Static_assert(sizeof(struct lregion_query) == 104, "PROCMAP_QUERY ABI");
+_Static_assert(sizeof(struct lregion_short_info) == 48, "short region reply ABI");
+
+extern kern_return_t __darling_rpc_mach_vm_region_recurse(mach_port_name_t target, unsigned long long* address, unsigned long long* size,
+ unsigned int* depth, int* info, unsigned int* infoCnt);
+
+/* Pacing. The one heavy caller is a loop that asks again as soon as it has an answer: through the server it made
+ * ~9,000 calls a second using 17% of a core; answered locally without a pause it made several times more, took 85% of a
+ * core, and fights got slower (61 vs 68 fps, 2026-10-10) although the server load halved. A call that follows the
+ * previous one within the pace interval waits out the rest of it, so the call rate stays what it was through the
+ * server while neither the server nor a core is spent on it. Isolated calls are not delayed. Host file
+ * /tmp/rclol-region-pace-us overrides the interval (0 = no pacing). */
+#define LREGION_DEFAULT_PACE_US 50 /* plus ~50 us of timer slack: ~105 us per call, the rate measured through the server */
+static volatile long lregion_pace_us = LREGION_DEFAULT_PACE_US;
+static volatile unsigned long long lregion_last_ns;
+
+static void lregion_pace(void)
+{
+ long pace = __atomic_load_n(&lregion_pace_us, __ATOMIC_RELAXED);
+ if (pace <= 0)
+ return;
+ struct { long tv_sec; long tv_nsec; } now, rest;
+ LINUX_SYSCALL(__NR_clock_gettime, 1 /* CLOCK_MONOTONIC */, &now);
+ unsigned long long t = now.tv_sec * 1000000000ull + now.tv_nsec;
+ unsigned long long last = __atomic_exchange_n(&lregion_last_ns, t, __ATOMIC_RELAXED);
+ unsigned long long interval = (unsigned long long) pace * 1000ull;
+ if (t >= last && t - last < interval) {
+ rest.tv_sec = 0;
+ rest.tv_nsec = (long) (interval - (t - last));
+ LINUX_SYSCALL(__NR_nanosleep, &rest, 0UL);
+ LINUX_SYSCALL(__NR_clock_gettime, 1, &now);
+ __atomic_store_n(&lregion_last_ns, now.tv_sec * 1000000000ull + now.tv_nsec, __ATOMIC_RELAXED);
+ }
+}
+
+VISIBLE
+kern_return_t mach_vm_region_recurse(mach_port_name_t target, unsigned long long* address, unsigned long long* size,
+ unsigned int* depth, int* info, unsigned int* infoCnt)
+{
+ int state = __atomic_load_n(&lregion_state, __ATOMIC_ACQUIRE);
+ /* The switches are re-read a few times per second only in a process where the feature can be on (same-match A/B).
+ * Everywhere else the first decision is final and this function makes no further system calls of its own:
+ * LeagueClient runs the same scanner, and periodic file opens there were the one new behaviour in that process
+ * when it died twice with an invalid RPC descriptor (2026-10-10). */
+ if (state == 0 || (lregion_refresh && (__atomic_fetch_add(&lregion_calls, 1, __ATOMIC_RELAXED) & 1023) == 0)) {
+ if (state == 0)
+ lregion_refresh = LINUX_SYSCALL(__NR_access, "/tmp/rclol-local-region-all", 0) == 0 || darling_is_league_game();
+ if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-local-region-all", 0) == 0)
+ state = 1;
+ else if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-no-local-region", 0) != 0 && darling_is_league_game())
+ state = 1;
+ else
+ state = -1;
+ __atomic_store_n(&lregion_state, state, __ATOMIC_RELEASE);
+ /* pacing in microseconds (host file, optional): see below */
+ long pfd = LINUX_SYSCALL(__NR_open, "/tmp/rclol-region-pace-us", 0, 0);
+ if (pfd >= 0) {
+ char digits[16];
+ long n = LINUX_SYSCALL(__NR_read, pfd, digits, sizeof(digits) - 1);
+ LINUX_SYSCALL(__NR_close, pfd);
+ long value = 0;
+ for (long i = 0; i < n && digits[i] >= '0' && digits[i] <= '9'; ++i) value = value * 10 + (digits[i] - '0');
+ if (n > 0 && value <= 100000) __atomic_store_n(&lregion_pace_us, value, __ATOMIC_RELAXED);
+ } else {
+ __atomic_store_n(&lregion_pace_us, LREGION_DEFAULT_PACE_US, __ATOMIC_RELAXED);
+ }
+ }
+ /* Any count up to the long structure (19 words): the server only fills the short one (12) and returns the others
+ * zeroed, with the region's address and size (checked with tools/riot-region-count19-check.c). The scanner in
+ * League asks with the long count, so the short-only version of this path never ran in a match. */
+ if (state == 1 && target == mach_task_self() && address && size && info && infoCnt &&
+ *infoCnt >= 1 && *infoCnt <= 19) {
+ /* One descriptor, opened once and kept at a high number. It was opened and closed for every call before
+ * (~9,000 times a second), which churned the low descriptor numbers the rest of the process hands out.
+ * After a fork the inherited descriptor still names the parent's address space, so it is tied to the pid;
+ * if the application closed or replaced it, the ioctl fails and the call goes to the server. */
+ lregion_pace();
+ int fd = __atomic_load_n(&lregion_maps_fd, __ATOMIC_ACQUIRE);
+ int self = (int) LINUX_SYSCALL(__NR_getpid);
+ if (fd < 0 || __atomic_load_n(&lregion_maps_pid, __ATOMIC_ACQUIRE) != self) {
+ int low = LINUX_SYSCALL(__NR_open, "/proc/self/maps", 02000000 /* O_RDONLY | O_CLOEXEC */, 0);
+ fd = -1;
+ if (low >= 0) {
+ fd = LINUX_SYSCALL(__NR_fcntl, low, 1030 /* F_DUPFD_CLOEXEC */, 900);
+ if (fd >= 0)
+ LINUX_SYSCALL(__NR_close, low);
+ else
+ fd = low;
+ __atomic_store_n(&lregion_maps_pid, self, __ATOMIC_RELEASE);
+ __atomic_store_n(&lregion_maps_fd, fd, __ATOMIC_RELEASE);
+ }
+ }
+ if (fd >= 0) {
+ struct lregion_query q;
+ for (unsigned long i = 0; i < sizeof(q); ++i) ((char*) &q)[i] = 0;
+ q.size = sizeof(q);
+ q.query_flags = 0x10; /* PROCMAP_QUERY_COVERING_OR_NEXT_VMA */
+ q.query_addr = *address;
+ long rv = LINUX_SYSCALL(__NR_ioctl, fd, LREGION_IOCTL, &q);
+ if (rv < 0 && rv != -LINUX_ENOENT)
+ __atomic_store_n(&lregion_maps_fd, -1, __ATOMIC_RELEASE); /* not ours any more: reopen on the next call */
+ if (rv == -LINUX_ENOENT)
+ return KERN_NO_SPACE; /* MIG leaves all caller outputs untouched on failure. */
+ if (rv == 0 && *infoCnt != LREGION_SHORT_INFO_COUNT) {
+ for (unsigned int i = 0; i < *infoCnt; ++i) info[i] = 0;
+ *address = q.vma_start;
+ *size = q.vma_end - q.vma_start;
+ if (depth) *depth = 0;
+ return KERN_SUCCESS;
+ }
+ if (rv == 0) {
+ struct lregion_short_info* out = (struct lregion_short_info*) info;
+ for (unsigned long i = 0; i < sizeof(*out); ++i) ((char*) out)[i] = 0;
+ out->protection = q.vma_flags & 7;
+ out->offset = q.vma_offset;
+ /* Compatibility with duct-tape memory.c: the server fills the LONG structure,
+ * then MIG copies only its first 48 bytes for a short request. Its share_mode
+ * lands at byte 47 (short-structure trailing padding), not the short share_mode. */
+ ((unsigned char*)out)[47] = (q.vma_flags & 8) ? 4 : 2;
+ out->max_protection = VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE;
+ *address = q.vma_start;
+ *size = q.vma_end - q.vma_start;
+ if (depth) *depth = 0;
+ return KERN_SUCCESS;
+ }
+ }
+ }
+
+ return __darling_rpc_mach_vm_region_recurse(target, address, size, depth, info, infoCnt);
+}
+
kern_return_t semaphore_signal_trap_impl(
mach_port_name_t signal_name)
{
+ struct lsem* local = lsem_find(signal_name);
+ if (local)
+ return lsem_signal(local, signal_name, lsem_capture(local));
int code = dserver_rpc_semaphore_signal(signal_name);
+ int attempt;
+
+ for (attempt = 0; code == -111 && attempt < 8; attempt++) {
+ dserver_retry_sleep(attempt);
+ code = dserver_rpc_semaphore_signal(signal_name);
+ }
if (code < 0) {
+ /* darling-lol: fail SOFT, never abort. semaphore_signal is issued
+ * on every pthread exit (sys_bsdthread_terminate posts join_sem,
+ * 0 when no joiner); if the per-thread darlingserver RPC channel
+ * is already stale the RPC fails with -111, and aborting the
+ * whole process for a dying thread's goodbye-signal killed e.g.
+ * Chromium/Electron renderers (SIGILL in __simple_abort, then
+ * SIGABRT). The caller ignores the result; return the error. */
__simple_printf("semaphore_signal failed (internally): %d\n", code);
- __simple_abort();
}
return code;
}
-
+
kern_return_t semaphore_signal_all_trap_impl(
mach_port_name_t signal_name)
{
+ struct lsem* local = lsem_find(signal_name);
+ if (local)
+ return lsem_signal_all(local, signal_name, lsem_capture(local));
int code = dserver_rpc_semaphore_signal_all(signal_name);
+ int attempt;
+
+ for (attempt = 0; code == -111 && attempt < 8; attempt++) {
+ dserver_retry_sleep(attempt);
+ code = dserver_rpc_semaphore_signal_all(signal_name);
+ }
if (code < 0) {
+ /* darling-lol: fail soft, see semaphore_signal_trap_impl. */
__simple_printf("semaphore_signal_all failed (internally): %d\n", code);
- __simple_abort();
}
return code;
@@ -128,21 +980,33 @@ kern_return_t semaphore_signal_thread_trap_impl(
mach_port_name_t signal_name,
mach_port_name_t thread_name)
{
+ struct lsem* local = lsem_find(signal_name);
+ if (local)
+ return lsem_signal_thread(local, signal_name, lsem_capture(local), thread_name);
UNIMPLEMENTED_TRAP();
return KERN_FAILURE;
}
+
kern_return_t semaphore_wait_trap_impl(
mach_port_name_t wait_name)
{
+ struct lsem* local = lsem_find(wait_name);
+ if (local)
+ return lsem_wait(local, wait_name, lsem_capture(local), -1, 0);
+ unsigned long long slowStart = slowwait_enabled() ? slowwait_now() : 0;
int code = dserver_rpc_semaphore_wait(wait_name);
+ if (slowStart) {
+ slowwait_log("semaphore_wait", wait_name, slowStart, (void**) __builtin_frame_address(0), 0, 0);
+ }
if (code < 0) {
if (code == -LINUX_EINTR) {
return KERN_ABORTED;
}
+ /* darling-lol: fail soft (a waiter whose RPC channel died must
+ * get an error, not kill the process); see above. */
__simple_printf("semaphore_wait failed (internally): %d\n", code);
- __simple_abort();
}
return code;
@@ -152,14 +1016,33 @@ kern_return_t semaphore_wait_signal_trap_impl(
mach_port_name_t wait_name,
mach_port_name_t signal_name)
{
+ struct lsem* localWait = lsem_find(wait_name);
+ struct lsem* localSignal = lsem_find(signal_name);
+ if (localWait || localSignal) {
+ /* The wait goes first: XNU registers it on wait_name and only then signals signal_name (even when the
+ * wait did not block). A local wait can do that from inside its loop; with a server-side wait the
+ * signal has to be issued first, the only order the server can be given. */
+ if (localWait) {
+ struct lsem_post post;
+ post.s = localSignal;
+ post.name = signal_name;
+ post.gen = localSignal ? lsem_capture(localSignal) : 0;
+ post.done = 0;
+ return lsem_wait(localWait, wait_name, lsem_capture(localWait), -1, &post);
+ }
+ kern_return_t kr = semaphore_signal_trap_impl(signal_name);
+ if (kr != KERN_SUCCESS)
+ return kr;
+ return semaphore_wait_trap_impl(wait_name);
+ }
int code = dserver_rpc_semaphore_wait_signal(wait_name, signal_name);
if (code < 0) {
if (code == -LINUX_EINTR) {
return KERN_ABORTED;
}
+ /* darling-lol: fail soft; see above. */
__simple_printf("semaphore_wait_signal failed (internally): %d\n", code);
- __simple_abort();
}
return code;
@@ -170,14 +1053,24 @@ kern_return_t semaphore_timedwait_trap_impl(
unsigned int sec,
clock_res_t nsec)
{
+ struct lsem* local = lsem_find(wait_name);
+ if (local) {
+ if (nsec < 0 || nsec >= 1000000000)
+ return KERN_INVALID_VALUE; /* what the server's BAD_MACH_TIMESPEC check answers */
+ return lsem_wait(local, wait_name, lsem_capture(local), (long long) sec * 1000000000ll + nsec, 0);
+ }
+ unsigned long long slowStart = slowwait_enabled() ? slowwait_now() : 0;
int code = dserver_rpc_semaphore_timedwait(wait_name, sec, nsec);
+ if (slowStart) {
+ slowwait_log("semaphore_timedwait", wait_name, slowStart, (void**) __builtin_frame_address(0), 0, sec);
+ }
if (code < 0) {
if (code == -LINUX_EINTR) {
return KERN_ABORTED;
}
+ /* darling-lol: fail soft; see above. */
__simple_printf("semaphore_timedwait failed (internally): %d\n", code);
- __simple_abort();
}
return code;
@@ -189,14 +1082,32 @@ kern_return_t semaphore_timedwait_signal_trap_impl(
unsigned int sec,
clock_res_t nsec)
{
+ struct lsem* localWait = lsem_find(wait_name);
+ struct lsem* localSignal = lsem_find(signal_name);
+ if (localWait || localSignal) {
+ if (localWait) {
+ struct lsem_post post;
+ if (nsec < 0 || nsec >= 1000000000)
+ return KERN_INVALID_VALUE; /* what the server's BAD_MACH_TIMESPEC check answers */
+ post.s = localSignal;
+ post.name = signal_name;
+ post.gen = localSignal ? lsem_capture(localSignal) : 0;
+ post.done = 0;
+ return lsem_wait(localWait, wait_name, lsem_capture(localWait), (long long) sec * 1000000000ll + nsec, &post);
+ }
+ kern_return_t kr = semaphore_signal_trap_impl(signal_name);
+ if (kr != KERN_SUCCESS)
+ return kr;
+ return semaphore_timedwait_trap_impl(wait_name, sec, nsec);
+ }
int code = dserver_rpc_semaphore_timedwait_signal(wait_name, signal_name, sec, nsec);
if (code < 0) {
if (code == -LINUX_EINTR) {
return KERN_ABORTED;
}
+ /* darling-lol: fail soft; see above. */
__simple_printf("semaphore_timedwait_signal failed (internally): %d\n", code);
- __simple_abort();
}
return code;
@@ -514,6 +1425,12 @@ kern_return_t _kernelrpc_mach_port_construct_trap_impl(
__simple_printf("mach_port_construct failed (internally): %d\n", code);
__simple_abort();
}
+ #if 0 /* M58 diagnostic. */
+ if (code == KERN_SUCCESS && name != 0 && *name == MACH_PORT_NULL) {
+ __simple_printf("M58PC construct-success-null target=%u flags=0x%x\n",
+ (unsigned) target, (unsigned) options->flags);
+ }
+ #endif
return code;
}
@@ -673,16 +1590,16 @@ kern_return_t macx_backing_store_recovery_impl(
return KERN_FAILURE;
}
-extern void __linux_sched_yield();
+extern void __linux_sched_yield_site(const char* site);
boolean_t swtch_pri_impl(int pri)
{
- __linux_sched_yield();
+ __linux_sched_yield_site("SWTPRI");
return 0;
}
boolean_t swtch_impl(void)
{
- __linux_sched_yield();
+ __linux_sched_yield_site("SWTCH");
return 0;
}
@@ -692,12 +1609,53 @@ kern_return_t syscall_thread_switch_impl(
int option,
mach_msg_timeout_t option_time)
{
+ /* option_time is in milliseconds (mach_msg_timeout_t). */
struct timespec tv = {
- .tv_sec = 0,
- .tv_nsec = 1000000
+ .tv_sec = option_time / 1000,
+ .tv_nsec = (long) (option_time % 1000) * 1000000
};
- // Sleep for 1ms
- __linux_nanosleep(&tv, &tv);
+
+ /* darling-lol: opt-in trace (host file /tmp/rclol-select-trace) of who calls thread_switch and with which option:
+ * every 50th call writes thread id, option, time and up to 6 caller addresses (frame-pointer walk) to
+ * /tmp/rclol-select-trace.log. Addresses are resolved offline with the process's /proc/<pid>/maps. */
+ {
+ static int trace_enabled = -1;
+ static unsigned long calls;
+ if (trace_enabled < 0)
+ trace_enabled = LINUX_SYSCALL(__NR_access, "/tmp/rclol-select-trace", 0) == 0;
+ if (trace_enabled && (__atomic_add_fetch(&calls, 1, __ATOMIC_RELAXED) % 50) == 1) {
+ char line[320];
+ int used;
+ void** fp = (void**) __builtin_frame_address(0);
+ void* prev = (void*) fp;
+ void* frames[6] = {0};
+ int count = 0;
+ while (count < 6 && fp && ((unsigned long) fp & 7) == 0 && (void*) fp >= prev && (unsigned long) fp - (unsigned long) prev < (1UL << 20)) {
+ frames[count++] = fp[1];
+ prev = (void*) fp;
+ fp = (void**) fp[0];
+ }
+ used = __simple_sprintf(line, "thread_switch pid=%d tid=%d n=%lu option=%d time=%u frames:",
+ (int) LINUX_SYSCALL0(__NR_getpid), (int) LINUX_SYSCALL0(__NR_gettid), calls, option, option_time);
+ for (int i = 0; i < count; ++i)
+ used += __simple_sprintf(line + used, " %lx", (unsigned long) frames[i]);
+ line[used++] = '\n';
+ line[used] = 0;
+ int fd = LINUX_SYSCALL(__NR_open, "/tmp/rclol-select-trace.log", 0x441, 0644);
+ if (fd >= 0) {
+ LINUX_SYSCALL(__NR_write, fd, line, used);
+ LINUX_SYSCALL1(__NR_close, fd);
+ }
+ }
+ }
+
+ /* darling-lol perf: a plain yield (option_time == 0) only gives up the CPU; sleeping a
+ * full millisecond there is longer than a frame budget at high refresh rates. Timed
+ * switches (spin locks pass 1 ms) keep sleeping for the requested time. */
+ if (option_time == 0)
+ __linux_sched_yield_site("THRSW");
+ else
+ __linux_nanosleep(&tv, &tv);
// TODO: we could implement this with yield_to() in LKM
return KERN_SUCCESS;
diff --git a/libsyscall/CMakeLists.txt b/libsyscall/CMakeLists.txt
index 57cebad..3014255 100755
--- a/libsyscall/CMakeLists.txt
+++ b/libsyscall/CMakeLists.txt
@@ -104,6 +104,18 @@ foreach(MIG_FILENAME IN LISTS LIBSYSCALL_MIGS)
)
endforeach()
+# emulation/src/xnu_syscall/mach/impl/mach_traps.c wraps semaphore_create/semaphore_destroy
+# (process-local semaphores) and mach_vm_region_recurse (own task). The MIG stubs get private
+# names, which the wrappers call.
+foreach(MIG_ARCH i386 x86_64)
+ set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/mach/task-${MIG_ARCH}-User.c" PROPERTIES
+ COMPILE_DEFINITIONS "semaphore_create=__darling_rpc_semaphore_create;semaphore_destroy=__darling_rpc_semaphore_destroy"
+ )
+ set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/mach/mach_vm-${MIG_ARCH}-User.c" PROPERTIES
+ COMPILE_DEFINITIONS "mach_vm_region_recurse=__darling_rpc_mach_vm_region_recurse"
+ )
+endforeach()
+
# mach/exc also needs the server component
list(APPEND syscall_mig_sources_32 "${CMAKE_CURRENT_BINARY_DIR}/mach/exc-i386-Server.c")
list(APPEND syscall_mig_sources_64 "${CMAKE_CURRENT_BINARY_DIR}/mach/exc-x86_64-Server.c")
@@ -179,6 +191,7 @@ set(syscall_sources
wrappers/csr.c
wrappers/fs_snapshot.c
wrappers/__get_cpu_capabilities.S
+ wrappers/__sysctlbyname.S
wrappers/gethostuuid.c
wrappers/getiopolicy_np.c
wrappers/guarded_open_dprotected_np.c
diff --git a/libsyscall/custom/errno.c b/libsyscall/custom/errno.c
index 88658cb..83ce61c 100644
--- a/libsyscall/custom/errno.c
+++ b/libsyscall/custom/errno.c
@@ -62,6 +62,12 @@ __attribute__((noinline))
cerror_return_t
cerror_nocancel(int err)
{
+#ifdef DARLING
+ /* ulock's ULF_NO_ERRNO path marks its error with bit 0x800. Its
+ * callers need -errno as the result, with thread errno untouched. */
+ if (err & 0x800)
+ return (cerror_return_t)(int64_t)(-(err & ~0x800));
+#endif
errno = err;
int *tsderrno = (int*)_os_tsd_get_direct(__TSD_ERRNO);
if (tsderrno) {
diff --git a/libsyscall/os/tsd.h b/libsyscall/os/tsd.h
index 6619381..78d90df 100644
--- a/libsyscall/os/tsd.h
+++ b/libsyscall/os/tsd.h
@@ -63,6 +63,22 @@ static __inline__ unsigned int
_os_cpu_number(void)
{
#ifdef DARLING
+#if defined(__x86_64__)
+ /* Linux keeps (node << 12) | cpu in IA32_TSC_AUX, which RDTSCP returns without entering the kernel: this is
+ * what the kernel's own vDSO getcpu does. The syscall below costs ~470 ns and libmalloc asks for the CPU
+ * number ~30,000 times a second on the game's main thread. Verified against sched_getcpu on every core. */
+ static int __darling_has_rdtscp = -1;
+ if (__builtin_expect(__darling_has_rdtscp < 0, 0)) {
+ unsigned int a, b, c, d;
+ __asm__ volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(0x80000001), "c"(0));
+ __darling_has_rdtscp = (d >> 27) & 1;
+ }
+ if (__builtin_expect(__darling_has_rdtscp, 1)) {
+ unsigned int lo, hi, aux;
+ __asm__ volatile("rdtscp" : "=a"(lo), "=d"(hi), "=c"(aux));
+ return aux & 0xfff;
+ }
+#endif
extern int __linux_syscall(int nr, ...);
unsigned int cpu_num = 0;
diff --git a/libsyscall/wrappers/__commpage_gettimeofday.c b/libsyscall/wrappers/__commpage_gettimeofday.c
index ccd2b33..1a1a7c6 100644
--- a/libsyscall/wrappers/__commpage_gettimeofday.c
+++ b/libsyscall/wrappers/__commpage_gettimeofday.c
@@ -66,6 +66,12 @@ __commpage_gettimeofday_internal(struct timeval *tp, uint64_t *tbr_out)
do {
TimeStamp_tick = *gtod_TimeStamp_tick_p;
+ /* An unpublished timestamp requires the syscall fallback. Avoid
+ * reading absolute time first, especially on syscall-only clocks.
+ */
+ if (TimeStamp_tick == 0) {
+ return 1;
+ }
/*
* This call contains an instruction barrier which will ensure that the
* second read of the abs time isn't speculated above the reads of the
diff --git a/libsyscall/wrappers/__sysctlbyname.S b/libsyscall/wrappers/__sysctlbyname.S
new file mode 100644
index 0000000..f185f6c
--- /dev/null
+++ b/libsyscall/wrappers/__sysctlbyname.S
@@ -0,0 +1,12 @@
+/*
+ * __sysctlbyname wrapper
+ * Bridges libc's sysctlbyname to the syscall stub ___sysctlbyname
+ */
+
+.text
+.globl __sysctlbyname
+.globl ___sysctlbyname
+
+__sysctlbyname:
+ /* Just jump to the syscall stub */
+ jmp ___sysctlbyname
diff --git a/darling/src/libsystem_kernel/emulation/include/common/cpu_frequency.h b/darling/src/libsystem_kernel/emulation/include/common/cpu_frequency.h
new file mode 100644
index 0000000..eb90f43
--- /dev/null
+++ b/darling/src/libsystem_kernel/emulation/include/common/cpu_frequency.h
@@ -0,0 +1,21 @@
+#ifndef DARLING_CPU_FREQUENCY_H
+#define DARLING_CPU_FREQUENCY_H
+#include <stdint.h>
+#include <stdbool.h>
+
+// Linux cpufreq exposes kHz; Darwin's hw.cpufrequency is an unsigned 64-bit Hz value.
+static inline bool darling_frequency_hz(const char* text, uint64_t* result)
+{
+ uint64_t value = 0;
+ if (!text || !result || *text < '0' || *text > '9') return false;
+ do {
+ unsigned digit = (unsigned)(*text++ - '0');
+ if (value > (UINT64_MAX - digit) / 10) return false;
+ value = value * 10 + digit;
+ } while (*text >= '0' && *text <= '9');
+ while (*text == ' ' || *text == '\t' || *text == '\r' || *text == '\n') ++text;
+ if (*text || !value || value > UINT64_MAX / 1000) return false;
+ *result = value * 1000;
+ return true;
+}
+#endif
diff --git a/darling/src/libsystem_kernel/emulation/src/linux_premigration/vchroot_userspace.c b/darling/src/libsystem_kernel/emulation/src/linux_premigration/vchroot_userspace.c
--- a/darling/src/libsystem_kernel/emulation/src/linux_premigration/vchroot_userspace.c
+++ b/darling/src/libsystem_kernel/emulation/src/linux_premigration/vchroot_userspace.c
@@ -421,12 +421,14 @@
ctxt->follow = true;
ctxt->symlink_depth++;
- // resolve absolute symlinks within the prefix
+ // Keep absolute host symlinks outside the prefix after crossing
+ // /Volumes/SystemRoot (e.g. /etc/resolv.conf -> /run/...).
+ // Resolve guest symlinks within the prefix
//
// however, DON'T resolve `/proc` symlinks within the prefix.
// anything under `/proc` is from Linux's `/proc` since we bind-mount it, so we can special-case them because we want those to be Linux-root relative.
// see https://github.com/darlinghq/darling/issues/1052 for the issue this works around.
- if (link[0] == '/' && !(strncmp(link, "/proc/", 6) == 0 || (rv == 5 && strncmp(link, "/proc", 5) == 0))) {
+ if (link[0] == '/' && ctxt->current_root_len > 0 && !(strncmp(link, "/proc/", 6) == 0 || (rv == 5 && strncmp(link, "/proc", 5) == 0))) {
ctxt->current_root = prefix_path;
ctxt->current_root_len = prefix_path_len;
}
diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/ioctl/socket.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/ioctl/socket.c
index f890ab2..8969577 100644
--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/ioctl/socket.c
+++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/ioctl/socket.c
@@ -2,12 +2,29 @@
#include <sys/errno.h>
#include <sys/socket.h>
-#include <ifaddrs.h>
#include <net/if.h>
+#include <net/if_media.h>
#include <netinet/in.h>
#include <sys/sockio.h>
#include <netinet6/in6_var.h>
+#include <darling/emulation/xnu_syscall/bsd/impl/ioctl/ioctl.h>
+
+extern int strcmp(const char* a, const char* b);
+extern int strncmp(const char* a, const char* b, size_t n);
+extern char* strncpy(char* destination, const char* source, size_t length);
+
+#define LINUX_SIOCGIFFLAGS 0x8913
+#define LINUX_IFNAMSIZ 16
+
+struct linux_ifreq {
+ char lifr_name[LINUX_IFNAMSIZ];
+ union {
+ short lifr_flags;
+ char lifr_pad[24];
+ };
+};
+
int handle_socket(int fd, unsigned int cmd, void* arg, int* retval)
{
switch (cmd)
@@ -19,6 +36,43 @@ int handle_socket(int fd, unsigned int cmd, void* arg, int* retval)
case SIOCAIFADDR_IN6: // set IPv6 address
*retval = -ENOTSUP;
return IOCTL_HANDLED;
+ case SIOCGIFMEDIA:
+#ifdef SIOCGIFXMEDIA
+ case SIOCGIFXMEDIA:
+#endif
+ {
+ struct ifmediareq* ifmr = (struct ifmediareq*) arg;
+ struct linux_ifreq ifr;
+ int status;
+
+ if (ifmr == NULL)
+ {
+ *retval = -EINVAL;
+ return IOCTL_HANDLED;
+ }
+
+ ifr.lifr_name[LINUX_IFNAMSIZ - 1] = '\0';
+ strncpy(ifr.lifr_name, ifmr->ifm_name, LINUX_IFNAMSIZ - 1);
+ status = __real_ioctl(fd, LINUX_SIOCGIFFLAGS, &ifr);
+ if (status < 0)
+ {
+ *retval = status;
+ return IOCTL_HANDLED;
+ }
+
+ if (strncmp(ifmr->ifm_name, "wl", 2) == 0)
+ ifmr->ifm_current = IFM_IEEE80211 | IFM_AUTO;
+ else
+ ifmr->ifm_current = IFM_ETHER | IFM_AUTO;
+ ifmr->ifm_mask = 0;
+ ifmr->ifm_status = ((ifr.lifr_flags & IFF_UP) && (ifr.lifr_flags & IFF_RUNNING))
+ ? (IFM_AVALID | IFM_ACTIVE) : IFM_AVALID;
+ ifmr->ifm_active = ifmr->ifm_current;
+ ifmr->ifm_count = 0;
+ ifmr->ifm_ulist = NULL;
+ *retval = 0;
+ return IOCTL_HANDLED;
+ }
}
return IOCTL_PASS;
}