Files
darling-lol/patches/xnu.patch
T
3AvisD 3309f079c7 patches, tools, docs: use neutral integrity-check wording and state the compatibility scope
The patch comments and the Mach-O strings filter named an anti-cheat product. Use neutral wording; the patches still differ only in comments. The platform-identity document now says the overrides are for compatibility, not a bypass of anti-cheat, telemetry, licensing or platform controls.
2026-10-19 22:47:59 -06:00

6203 lines
248 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 client logged integrity-check 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;
}