kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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 }