tbd_read.c (11684B)
1 /* Apple `.tbd` (text-based stub) reader. 2 * 3 * `.tbd` files describe a dylib's ABI surface in a YAML-shaped TAPI 4 * format — Apple ships them in the macOS SDK as a substitute for the 5 * actual .dylib bytes. kit's linker treats them as a peer of 6 * read_macho_dso: extract install-name + the exported symbol set, and 7 * surface that as an ObjBuilder full of defined OBJ_SEC_NONE entries. 8 * 9 * The TAPI grammar is intricate (per-target `exports:` blocks, weak 10 * symbols, Obj-C metadata, re-exports, ...). Rather than re-implement 11 * a YAML parser, this reader takes the conservative approach: it 12 * extracts the FIRST document's `install-name:` (the umbrella the 13 * consumer records in LC_LOAD_DYLIB) and then **scans the entire file 14 * for symbol-looking tokens** — sequences starting with `_` followed by 15 * identifier chars. The result is the union of every C, Obj-C class, 16 * weak, and re-exported symbol declared anywhere in the file. 17 * 18 * Why the union is safe: 19 * - kit's linker only consults the DSO's exported set to satisfy 20 * undefs. Including a symbol the consumer never references is 21 * harmless — the symbol simply never appears in our output's 22 * LC_LOAD_DYLIB chain. 23 * - dyld at runtime walks libSystem's full re-export graph to bind 24 * each name; our static-link decision (which dylib provides it) 25 * reduces to "the umbrella" anyway. We only need to convince the 26 * static linker that the name is bindable, then write 27 * LC_LOAD_DYLIB against the umbrella's install-name. 28 * 29 * The scanner skips the top-of-file `install-name:` line so its path 30 * (`/usr/lib/libSystem.B.dylib`) doesn't end up as a fake symbol — but 31 * since paths don't start with `_`, that wasn't actually a risk. 32 * 33 * Identifier alphabet: A-Z, a-z, 0-9, `_`, `$`, `.`. This matches 34 * Apple's C / Obj-C symbol mangling (e.g. `'_OBJC_CLASS_$_NSString'`, 35 * `'_pause$NOCANCEL'`). Tokens may be surrounded by single or double 36 * quotes — the scanner doesn't see those, since they aren't in the 37 * identifier alphabet, so a token like `'_pause$NOCANCEL'` matches as 38 * just `_pause$NOCANCEL`. */ 39 40 #include <string.h> 41 42 #include "core/heap.h" 43 #include "core/pool.h" 44 #include "core/slice.h" 45 #include "obj/obj.h" 46 47 static int is_id_start(u8 c) { return c == '_'; } 48 static int is_id_cont(u8 c) { 49 return (c == '_') || (c == '$') || (c == '.') || (c >= 'A' && c <= 'Z') || 50 (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); 51 } 52 53 static int is_target_cont(u8 c) { 54 return is_id_cont(c) || c == '-'; 55 } 56 57 static int tbd_line_has_token(const u8* data, size_t start, size_t end, 58 const char* token, size_t token_len) { 59 size_t i; 60 for (i = start; i + token_len <= end; ++i) { 61 size_t after = i + token_len; 62 if ((i == start || !is_target_cont(data[i - 1u])) && 63 memcmp(data + i, token, token_len) == 0 && 64 (after == end || !is_target_cont(data[after]))) 65 return 1; 66 } 67 return 0; 68 } 69 70 static int tbd_line_has_target(const u8* data, size_t start, size_t end, 71 const char* arch, size_t arch_len, 72 const char* platform, size_t platform_len, 73 int allow_arm64e) { 74 size_t i; 75 for (i = start; i + arch_len + platform_len <= end; ++i) { 76 size_t after = i + arch_len + platform_len; 77 if ((i == start || !is_target_cont(data[i - 1u])) && 78 memcmp(data + i, arch, arch_len) == 0 && 79 memcmp(data + i + arch_len, platform, platform_len) == 0 && 80 (after == end || !is_target_cont(data[after]))) 81 return 1; 82 /* Current macOS SDK stubs advertise arm64e for system dylibs even when an 83 * ordinary arm64 consumer is linked. ld64 accepts that ABI-compatible 84 * provider, so Kit must accept the SDK's only ARM slice as well. */ 85 if (allow_arm64e && i + 6u + platform_len <= end && 86 (i == start || !is_target_cont(data[i - 1u])) && 87 memcmp(data + i, "arm64e", 6u) == 0 && 88 memcmp(data + i + 6u, platform, platform_len) == 0 && 89 (i + 6u + platform_len == end || 90 !is_target_cont(data[i + 6u + platform_len]))) 91 return 1; 92 } 93 return 0; 94 } 95 96 /* TAPI v4 declares architecture/platform pairs in top-level `targets:`. v2/v3 97 * split the same authority across top-level `archs:` and `platform:` fields. 98 * Validate either schema before treating the text stub as a DSO; only truly 99 * target-less legacy documents retain the permissive scanner behavior. */ 100 static int tbd_supports_target(Compiler* c, const u8* data, size_t len, 101 int* declared_out) { 102 static const char TARGETS_KEY[] = "targets:"; 103 static const char ARCHS_KEY[] = "archs:"; 104 static const char PLATFORM_KEY[] = "platform:"; 105 const char* arch; 106 const char* platform; 107 const char* legacy_platform; 108 size_t arch_len; 109 size_t platform_len; 110 size_t legacy_platform_len; 111 size_t line = 0; 112 int saw_archs = 0; 113 int saw_platform = 0; 114 int arch_matches = 0; 115 int platform_matches = 0; 116 117 *declared_out = 0; 118 switch (c->target.arch) { 119 case KIT_ARCH_ARM_64: 120 arch = "arm64"; 121 break; 122 case KIT_ARCH_X86_64: 123 arch = "x86_64"; 124 break; 125 default: 126 *declared_out = 1; 127 return 0; 128 } 129 switch (c->target.os) { 130 case KIT_OS_MACOS: 131 platform = "-macos"; 132 legacy_platform = "macosx"; 133 break; 134 case KIT_OS_IOS: 135 platform = "-ios"; 136 legacy_platform = "ios"; 137 break; 138 case KIT_OS_IOS_SIMULATOR: 139 platform = "-ios-simulator"; 140 legacy_platform = "ios-simulator"; 141 break; 142 default: 143 *declared_out = 1; 144 return 0; 145 } 146 arch_len = strlen(arch); 147 platform_len = strlen(platform); 148 legacy_platform_len = strlen(legacy_platform); 149 150 while (line < len) { 151 size_t end = line; 152 while (end < len && data[end] != '\n' && data[end] != '\r') ++end; 153 if (end - line >= sizeof(TARGETS_KEY) - 1u && 154 memcmp(data + line, TARGETS_KEY, sizeof(TARGETS_KEY) - 1u) == 0) { 155 *declared_out = 1; 156 return tbd_line_has_target( 157 data, line + sizeof(TARGETS_KEY) - 1u, end, arch, arch_len, platform, 158 platform_len, c->target.arch == KIT_ARCH_ARM_64); 159 } 160 if (end - line >= sizeof(ARCHS_KEY) - 1u && 161 memcmp(data + line, ARCHS_KEY, sizeof(ARCHS_KEY) - 1u) == 0) { 162 size_t value = line + sizeof(ARCHS_KEY) - 1u; 163 saw_archs = 1; 164 arch_matches = 165 tbd_line_has_token(data, value, end, arch, arch_len) || 166 (c->target.arch == KIT_ARCH_ARM_64 && 167 tbd_line_has_token(data, value, end, "arm64e", 6u)); 168 } 169 if (end - line >= sizeof(PLATFORM_KEY) - 1u && 170 memcmp(data + line, PLATFORM_KEY, sizeof(PLATFORM_KEY) - 1u) == 0) { 171 size_t value = line + sizeof(PLATFORM_KEY) - 1u; 172 saw_platform = 1; 173 platform_matches = tbd_line_has_token( 174 data, value, end, legacy_platform, legacy_platform_len); 175 if (!platform_matches && c->target.os == KIT_OS_MACOS) 176 platform_matches = 177 tbd_line_has_token(data, value, end, "macos", 5u); 178 } 179 while (end < len && (data[end] == '\n' || data[end] == '\r')) ++end; 180 line = end; 181 } 182 *declared_out = saw_archs || saw_platform; 183 return (!saw_archs || arch_matches) && 184 (!saw_platform || platform_matches); 185 } 186 187 /* Extract the install-name from the first document. We look for a 188 * line beginning with "install-name:" and take the value up to EOL, 189 * then strip whitespace and surrounding quotes. Returns 0 if absent. */ 190 static Sym extract_install_name(Compiler* c, const u8* data, size_t len) { 191 static const char KEY[] = "install-name:"; 192 size_t klen = sizeof(KEY) - 1u; 193 for (size_t i = 0; i + klen <= len; ++i) { 194 /* Match at start of line (i==0 or preceded by '\n'). */ 195 if (i > 0 && data[i - 1] != '\n') continue; 196 if (memcmp(data + i, KEY, klen) != 0) continue; 197 /* Skip past the colon and surrounding whitespace. */ 198 size_t j = i + klen; 199 while (j < len && (data[j] == ' ' || data[j] == '\t')) ++j; 200 /* Take to EOL. */ 201 size_t start = j; 202 while (j < len && data[j] != '\n' && data[j] != '\r') ++j; 203 size_t end = j; 204 /* Strip trailing whitespace. */ 205 while (end > start && (data[end - 1] == ' ' || data[end - 1] == '\t' || 206 data[end - 1] == '\r')) 207 --end; 208 /* Strip surrounding single or double quotes. */ 209 if (end > start + 1u && (data[start] == '\'' || data[start] == '"') && 210 data[end - 1] == data[start]) { 211 ++start; 212 --end; 213 } 214 if (end > start) 215 return pool_intern_slice( 216 c->global, 217 (Slice){.s = (const char*)(data + start), .len = (u32)(end - start)}); 218 return 0; 219 } 220 return 0; 221 } 222 223 ObjBuilder* read_tbd(Compiler* c, const char* name, const u8* data, size_t len, 224 Sym* install_name_out) { 225 int declares_targets; 226 if (install_name_out) *install_name_out = 0; 227 if (!data || !len) compiler_panic(c, SRCLOC_NONE, "read_tbd: empty input"); 228 229 /* Validate magic: a tbd starts with `--- !tapi-tbd` (or any `---`). */ 230 if (len < 4 || data[0] != '-' || data[1] != '-' || data[2] != '-') 231 compiler_panic(c, SRCLOC_NONE, "read_tbd: not a tbd file (missing '---')"); 232 233 /* Reject obviously-wrong target arches up front so we don't stream a 234 * bunch of irrelevant symbols in. */ 235 switch (c->target.arch) { 236 case KIT_ARCH_ARM_64: 237 case KIT_ARCH_X86_64: 238 break; 239 default: 240 compiler_panic(c, SRCLOC_NONE, 241 "read_tbd: unsupported target arch %u for tbd lookup", 242 (u32)c->target.arch); 243 } 244 if (!tbd_supports_target(c, data, len, &declares_targets) && 245 declares_targets) { 246 const char* arch = 247 c->target.arch == KIT_ARCH_ARM_64 ? "arm64" : "x86_64"; 248 const char* platform = c->target.os == KIT_OS_IOS_SIMULATOR 249 ? "ios-simulator" 250 : (c->target.os == KIT_OS_IOS ? "ios" 251 : "macos"); 252 compiler_panic(c, SRCLOC_NONE, 253 "read_tbd: input '%s' does not support target %s-%s", 254 name ? name : "(unnamed)", arch, platform); 255 } 256 257 ObjBuilder* ob = obj_new(c); 258 if (!ob) compiler_panic(c, SRCLOC_NONE, "read_tbd: obj_new failed"); 259 260 if (install_name_out) *install_name_out = extract_install_name(c, data, len); 261 262 /* Token scanner: walk the file, emit every `_id` token as a defined 263 * external ObjSymbol. Tracking already-seen names via a tiny linear 264 * dedup list would be linear-quadratic on a multi-MB tbd; instead we 265 * rely on the pool's intern de-dup downstream — duplicate ObjSymbol 266 * names are tolerated by the linker's hash, with the second insert 267 * resolving to the existing entry on collision. */ 268 size_t i = 0; 269 while (i < len) { 270 /* Skip non-token bytes. */ 271 while (i < len && !is_id_start(data[i])) ++i; 272 if (i >= len) break; 273 size_t start = i; 274 while (i < len && is_id_cont(data[i])) ++i; 275 size_t tlen = i - start; 276 if (tlen == 0) continue; 277 /* Filter out the obvious YAML-key-like collisions: tokens that are 278 * field names ("_macos" doesn't occur, but be defensive). All 279 * Apple symbols start with `_` followed by another id char, so we 280 * keep tokens of length >= 2. Single `_` is the throwaway-name 281 * convention and never an exported symbol. */ 282 if (tlen < 2u) continue; 283 Sym sn = pool_intern_slice( 284 c->global, (Slice){.s = (const char*)(data + start), .len = (u32)tlen}); 285 obj_symbol_ex(ob, sn, SB_GLOBAL, SV_DEFAULT, SK_NOTYPE, OBJ_SEC_NONE, 0, 0, 286 0); 287 } 288 289 obj_finalize(ob); 290 return ob; 291 }