macos.c (7264B)
1 /* macOS-specific env bits: mach_vm_remap dual-mapped exec memory, 2 * sys_icache_invalidate for the dbg flush path, st_mtimespec mtime, 3 * dlsym leading-underscore handling, host_target OS=MACOS/OBJ=MACHO. 4 * 5 * Compiled only on Darwin; the Makefile selects this file via 6 * HOST_UNAME=Darwin. No #ifdef inside. 7 */ 8 9 #include <dlfcn.h> 10 #include <libkern/OSCacheControl.h> 11 #include <mach-o/dyld.h> 12 #include <mach/mach.h> 13 #include <mach/mach_vm.h> 14 #include <mach/vm_map.h> 15 #include <stdint.h> 16 #include <stdlib.h> 17 #include <string.h> 18 #include <sys/mman.h> 19 #include <sys/stat.h> 20 21 #include "env_posix.h" 22 23 /* ---------------- dual-mapped exec memory ---------------- */ 24 /* mach_vm_remap creates a second VA pointing at the same physical memory. 25 * The write alias is RW; the runtime alias is left at PROT_READ until 26 * protect() flips it to PROT_READ|PROT_EXEC. No MAP_JIT, no per-thread 27 * mode bit, no entitlement coupling for unsigned dev binaries (signed 28 * hardened-runtime binaries need com.apple.security.cs.allow-unsigned- 29 * executable-memory). */ 30 KitStatus os_execmem_reserve_exec(size_t size, KitExecMemRegion* out) { 31 void* w; 32 mach_vm_address_t r_addr = 0; 33 vm_prot_t cur = 0, max = 0; 34 kern_return_t kr; 35 ExecMemToken* tok; 36 37 w = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); 38 if (w == MAP_FAILED) return KIT_NOMEM; 39 40 /* copy=FALSE: the new VA shares the same physical pages. 41 * VM_INHERIT_NONE: child processes don't observe the executable alias. */ 42 kr = mach_vm_remap(mach_task_self(), &r_addr, (mach_vm_size_t)size, 43 /*mask=*/0, VM_FLAGS_ANYWHERE, mach_task_self(), 44 (mach_vm_address_t)(uintptr_t)w, 45 /*copy=*/FALSE, &cur, &max, VM_INHERIT_NONE); 46 if (kr != KERN_SUCCESS) { 47 munmap(w, size); 48 return KIT_ERR; 49 } 50 51 /* Drop the runtime alias to PROT_READ until protect() flips it. No 52 * window of writable+executable: write alias has W (no X), runtime 53 * alias has R only (no W, no X yet). */ 54 if (mprotect((void*)(uintptr_t)r_addr, size, PROT_READ) != 0) { 55 munmap((void*)(uintptr_t)r_addr, size); 56 munmap(w, size); 57 return KIT_ERR; 58 } 59 60 tok = (ExecMemToken*)malloc(sizeof(*tok)); 61 if (!tok) { 62 munmap((void*)(uintptr_t)r_addr, size); 63 munmap(w, size); 64 return KIT_NOMEM; 65 } 66 tok->write_addr = w; 67 tok->runtime_addr = (void*)(uintptr_t)r_addr; 68 tok->size = size; 69 70 exec_dual_register(w, (void*)(uintptr_t)r_addr, size); 71 72 out->write = w; 73 out->runtime = (void*)(uintptr_t)r_addr; 74 out->size = size; 75 out->token = tok; 76 return KIT_OK; 77 } 78 79 /* ---------------- dbg W^X dance ---------------- */ 80 /* Dual-mapped reservation: the write alias is a separate VA already RW. 81 * Translate via the registry; no protect flip required, so end is no-op. */ 82 KitStatus os_dbg_code_write_begin(void* user, void* runtime_addr, size_t n, 83 void** write_out) { 84 (void)user; 85 if (!runtime_addr || !n || !write_out) return KIT_INVALID; 86 return exec_dual_lookup(runtime_addr, n, write_out) == 0 ? KIT_OK : KIT_ERR; 87 } 88 89 void os_dbg_code_write_end(void* user, void* runtime_addr, size_t n) { 90 (void)user; 91 (void)runtime_addr; 92 (void)n; 93 } 94 95 /* sys_icache_invalidate is the documented Apple primitive on aarch64 and 96 * is a no-op on x86_64 (correctly modeling that x86 fetches are coherent 97 * with stores). Use it across arches for symmetry. */ 98 void os_dbg_flush_icache(void* user, void* runtime_addr, size_t n) { 99 (void)user; 100 sys_icache_invalidate(runtime_addr, n); 101 } 102 103 /* ---------------- st_mtim/mtimespec ---------------- */ 104 105 int os_stat_mtime_ns(const struct stat* sb, int64_t* out) { 106 *out = (int64_t)sb->st_mtimespec.tv_sec * 1000000000LL + 107 (int64_t)sb->st_mtimespec.tv_nsec; 108 return 0; 109 } 110 111 /* ---------------- dlsym name mangling ---------------- */ 112 /* On Mach-O the linker hands us C names with a leading underscore 113 * (obj_format_c_mangle), but dlsym(RTLD_DEFAULT) expects the source-level 114 * name. Try the stripped form first to avoid a wasted dlsym call per 115 * libc symbol. */ 116 void* os_dlsym(const char* name) { 117 void* p; 118 if (name[0] == '_' && name[1] != '\0') { 119 p = dlsym(RTLD_DEFAULT, name + 1); 120 if (p) return p; 121 } 122 return dlsym(RTLD_DEFAULT, name); 123 } 124 125 /* ---------------- host_target os/obj ---------------- */ 126 127 void os_host_target_fill(KitTargetSpec* t) { 128 t->os = KIT_OS_MACOS; 129 t->obj = KIT_OBJ_MACHO; 130 } 131 132 /* ---------------- self executable path ---------------- */ 133 /* _NSGetExecutablePath yields the path as invoked (may contain symlinks or 134 * `..`); realpath canonicalizes it to a stable absolute path so installed 135 * links keep resolving regardless of how kit was launched. */ 136 int driver_self_exe_path(DriverEnv* env, char** out, size_t* out_size) { 137 char stackbuf[1024]; 138 char* nsbuf = stackbuf; 139 uint32_t cap = (uint32_t)sizeof stackbuf; 140 size_t heap_cap = 0; 141 char* resolved; 142 size_t size; 143 144 if (!env || !out || !out_size) return 1; 145 if (_NSGetExecutablePath(nsbuf, &cap) != 0) { 146 /* cap was set to the required size; allocate and retry once. */ 147 heap_cap = cap; 148 nsbuf = (char*)driver_alloc(env, heap_cap); 149 if (!nsbuf) return 1; 150 if (_NSGetExecutablePath(nsbuf, &cap) != 0) { 151 driver_free(env, nsbuf, heap_cap); 152 return 1; 153 } 154 } 155 resolved = realpath(nsbuf, NULL); /* malloc'd; caller frees with free() */ 156 if (heap_cap) driver_free(env, nsbuf, heap_cap); 157 if (!resolved) return 1; 158 159 size = driver_strlen(resolved) + 1u; 160 *out = (char*)driver_alloc(env, size); 161 if (!*out) { 162 free(resolved); 163 return 1; 164 } 165 driver_memcpy(*out, resolved, size); 166 *out_size = size; 167 free(resolved); 168 return 0; 169 } 170 171 /* ---------------- default hosted dirs probe ---------------- */ 172 /* With no --sysroot or KIT_SYSROOT, locate the macOS SDK by stat'ing the 173 * canonical Command Line Tools and Xcode.app SDK roots -- the same locations 174 * `xcrun --show-sdk-path` reports, without a subprocess. Pure on-disk 175 * discovery: the env-var override is KIT_SYSROOT, handled uniformly in 176 * driver/lib/hosted.c. Only the macOS host target is probed; cross-targets get 177 * nothing. The <sdk>/usr/{include,lib} mapping mirrors 178 * hosted_dirs_from_sysroot's macOS case in driver/lib/hosted.c (layering 179 * forbids calling into it from the env TU). */ 180 int driver_default_hosted_dirs(DriverEnv* env, KitTargetSpec target, 181 DriverHostedDirs* out) { 182 static const char* const candidates[] = { 183 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk", 184 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/" 185 "Developer/SDKs/MacOSX.sdk", 186 }; 187 const char* sdk = NULL; 188 size_t i; 189 (void)env; 190 if (target.os != KIT_OS_MACOS) return 1; 191 for (i = 0; i < sizeof(candidates) / sizeof(candidates[0]); ++i) { 192 if (driver_path_exists(candidates[i])) { 193 sdk = candidates[i]; 194 break; 195 } 196 } 197 if (!sdk) return 1; 198 /* The SDK is a single sysroot-shaped tree, so record it as the root (a static 199 * literal -- stable for the process). -print-sysroot reports it. */ 200 out->root = sdk; 201 if (driver_hosted_dirs_add_inc_join(out, sdk, "usr/include") != 0 || 202 driver_hosted_dirs_add_lib_join(out, sdk, "usr/lib") != 0) 203 return 1; 204 return out->nlibdirs ? 0 : 1; 205 }