kit

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

pe-import-smoke.c (15793B)


      1 /* PE import-directory smoke test — Phase 4.5 from doc/WINDOWS.md.
      2  *
      3  * Exercises the full chain:
      4  *   short-import shim bytes
      5  *     -> link_add_obj_bytes (reclassifies as DSO via OBJ_EXT_COFF
      6  *        annotation set by read_coff's short-import path)
      7  *     -> link_resolve (marks ExitProcess as imported, dso_input_id
      8  *        = the shim)
      9  *     -> link_emit_image_writer -> link_emit_coff (synthesizes
     10  *        .idata, IAT, per-arch IAT stub in .text)
     11  *
     12  * No execution — verification is byte-shape only via mingw's
     13  *   x86_64-w64-mingw32-objdump -p   (import directory / headers)
     14  *   x86_64-w64-mingw32-objdump -d   (disassembly of .text shows the
     15  *                                    call through the IAT stub)
     16  *
     17  * Skips cleanly with non-zero diagnostic-style message but exit 0
     18  * when the mingw objdump is not on PATH. */
     19 
     20 #include <kit/core.h>
     21 #include <kit/link.h>
     22 #include <kit/object.h>
     23 #include <setjmp.h>
     24 #include <stdarg.h>
     25 #include <stdio.h>
     26 #include <stdlib.h>
     27 #include <string.h>
     28 #include <unistd.h>
     29 
     30 #include "core/core.h"
     31 #include "core/pool.h"
     32 #include "link/link.h"
     33 #include "obj/obj.h"
     34 
     35 /* ---- short-import wire constants (mirror the spec in
     36  * test/coff/kit-roundtrip-coff.c::test_short_import_amd64). ---- */
     37 #define SHIM_HEADER_SIZE 20u
     38 #define SHIM_SYM_CSTR "ExitProcess"
     39 #define SHIM_DLL_CSTR "KERNEL32.dll"
     40 #define SHIM_SYM_NUL_LEN 12u /* "ExitProcess\0" */
     41 #define SHIM_DLL_NUL_LEN 13u /* "KERNEL32.dll\0" */
     42 #define SHIM_DATA_LEN (SHIM_SYM_NUL_LEN + SHIM_DLL_NUL_LEN) /* 25 */
     43 #define SHIM_TOTAL_LEN (SHIM_HEADER_SIZE + SHIM_DATA_LEN)   /* 45 */
     44 
     45 /* IMAGE_FILE_MACHINE_AMD64. */
     46 #define COFF_MACHINE_AMD64 0x8664u
     47 /* Sig1=0, Sig2=0xFFFF marks a short-import record. */
     48 #define COFF_SHIMP_SIG2 0xFFFFu
     49 /* TypeFlags = Type=CODE(0) | (NameType=NAME(1) << 2) = 0x0004. */
     50 #define COFF_SHIMP_TYPEFLAGS 0x0004u
     51 
     52 /* PE optional-header / data-directory constants we assert. */
     53 #define PE_DD_IDX_IMPORT 1
     54 #define PE_DD_IDX_IAT 12
     55 
     56 /* The exit-process program: e8 disp32 c3  (call ExitProcess; ret).
     57  * disp32 is patched by R_PC32 against an undef ExitProcess. */
     58 static const uint8_t PROG_TEXT_X64[6] = {0xe8, 0, 0, 0, 0, 0xc3};
     59 
     60 /* ---- env vtables --------------------------------------------------- */
     61 
     62 static void* heap_alloc(KitHeap* h, size_t n, size_t a) {
     63   (void)h;
     64   (void)a;
     65   return n ? malloc(n) : NULL;
     66 }
     67 static void* heap_realloc(KitHeap* h, void* p, size_t o, size_t n, size_t a) {
     68   (void)h;
     69   (void)o;
     70   (void)a;
     71   return realloc(p, n);
     72 }
     73 static void heap_free(KitHeap* h, void* p, size_t n) {
     74   (void)h;
     75   (void)n;
     76   free(p);
     77 }
     78 static KitHeap g_heap = {heap_alloc, heap_realloc, heap_free, NULL};
     79 
     80 static void diag_emit(KitDiagSink* s, KitDiagKind k, KitSrcLoc loc,
     81                       const char* fmt, va_list ap) {
     82   static const char* names[] = {"note", "warning", "error", "fatal"};
     83   (void)s;
     84   (void)loc;
     85   fprintf(stderr, "%s: ", names[k]);
     86   vfprintf(stderr, fmt, ap);
     87   fputc('\n', stderr);
     88 }
     89 static KitDiagSink g_diag = {diag_emit, NULL, 0, 0};
     90 
     91 /* ---- failure tracking --------------------------------------------- */
     92 
     93 static int g_failures;
     94 #define EXPECT(cond, ...)                                  \
     95   do {                                                     \
     96     if (!(cond)) {                                         \
     97       fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
     98       fprintf(stderr, __VA_ARGS__);                        \
     99       fputc('\n', stderr);                                 \
    100       g_failures++;                                        \
    101     }                                                      \
    102   } while (0)
    103 
    104 /* ---- target / compiler ------------------------------------------- */
    105 
    106 static KitContext g_ctx;
    107 
    108 static void target_x64_windows(KitTargetSpec* t) {
    109   memset(t, 0, sizeof *t);
    110   t->arch = KIT_ARCH_X86_64;
    111   t->os = KIT_OS_WINDOWS;
    112   t->obj = KIT_OBJ_COFF;
    113   t->ptr_size = 8;
    114   t->ptr_align = 8;
    115   t->big_endian = false;
    116   t->pic = KIT_PIC_PIE;
    117   t->code_model = KIT_CM_SMALL;
    118 }
    119 
    120 static Compiler* make_compiler(const KitTargetSpec* t) {
    121   memset(&g_ctx, 0, sizeof g_ctx);
    122   g_ctx.heap = &g_heap;
    123   g_ctx.diag = &g_diag;
    124   g_ctx.now = -1;
    125   KitTargetOptions opts;
    126   memset(&opts, 0, sizeof opts);
    127   opts.spec = *t;
    128   KitTarget* target = NULL;
    129   if (kit_target_new(&g_ctx, &opts, &target) != KIT_OK || !target) return NULL;
    130   KitCompiler* cc = NULL;
    131   if (kit_compiler_new(target, &g_ctx, &cc) != KIT_OK || !cc) {
    132     kit_target_free(target);
    133     return NULL;
    134   }
    135   return (Compiler*)cc;
    136 }
    137 
    138 static void free_compiler(Compiler* c) {
    139   if (!c) return;
    140   const KitTarget* target = kit_compiler_target((KitCompiler*)c);
    141   kit_compiler_free((KitCompiler*)c);
    142   kit_target_free((KitTarget*)target);
    143 }
    144 
    145 /* ---- short-import shim builder ------------------------------------ */
    146 
    147 static void build_short_import_amd64(uint8_t buf[SHIM_TOTAL_LEN]) {
    148   memset(buf, 0, SHIM_TOTAL_LEN);
    149   /* Sig1 = 0 (bytes 0..1 already 0). */
    150   /* Sig2 = 0xFFFF. */
    151   buf[2] = (uint8_t)(COFF_SHIMP_SIG2 & 0xFF);
    152   buf[3] = (uint8_t)((COFF_SHIMP_SIG2 >> 8) & 0xFF);
    153   /* Version = 0. */
    154   /* Machine. */
    155   buf[6] = (uint8_t)(COFF_MACHINE_AMD64 & 0xFF);
    156   buf[7] = (uint8_t)((COFF_MACHINE_AMD64 >> 8) & 0xFF);
    157   /* TimeDateStamp = 0 (bytes 8..11). */
    158   /* SizeOfData. */
    159   buf[12] = (uint8_t)(SHIM_DATA_LEN & 0xFFu);
    160   buf[13] = (uint8_t)((SHIM_DATA_LEN >> 8) & 0xFFu);
    161   buf[14] = (uint8_t)((SHIM_DATA_LEN >> 16) & 0xFFu);
    162   buf[15] = (uint8_t)((SHIM_DATA_LEN >> 24) & 0xFFu);
    163   /* OrdinalOrHint = 0 (16..17). */
    164   /* TypeFlags. */
    165   buf[18] = (uint8_t)(COFF_SHIMP_TYPEFLAGS & 0xFF);
    166   buf[19] = (uint8_t)((COFF_SHIMP_TYPEFLAGS >> 8) & 0xFF);
    167   /* Body: "ExitProcess\0" + "KERNEL32.dll\0". */
    168   memcpy(buf + SHIM_HEADER_SIZE, SHIM_SYM_CSTR, SHIM_SYM_NUL_LEN);
    169   memcpy(buf + SHIM_HEADER_SIZE + SHIM_SYM_NUL_LEN, SHIM_DLL_CSTR,
    170          SHIM_DLL_NUL_LEN);
    171 }
    172 
    173 /* ---- program ObjBuilder builder ----------------------------------- */
    174 
    175 static ObjBuilder* build_program(Compiler* c) {
    176   ObjBuilder* ob = obj_new(c);
    177   Pool* p = c->global;
    178   Sym text_name = pool_intern_slice(p, SLICE_LIT(".text"));
    179   Sym main_name = pool_intern_slice(p, SLICE_LIT("mainCRTStartup"));
    180   Sym exit_name = pool_intern_slice(p, SLICE_LIT(SHIM_SYM_CSTR));
    181   ObjSecId text = obj_section(ob, text_name, SEC_TEXT, SF_ALLOC | SF_EXEC, 16);
    182   obj_write(ob, text, PROG_TEXT_X64, sizeof PROG_TEXT_X64);
    183   /* mainCRTStartup at .text offset 0. */
    184   obj_symbol(ob, main_name, SB_GLOBAL, SK_FUNC, text, 0, sizeof PROG_TEXT_X64);
    185   /* ExitProcess as undef; reloc against the `e8` displacement (offset 1). */
    186   ObjSymId exit_sym =
    187       obj_symbol(ob, exit_name, SB_GLOBAL, SK_UNDEF, OBJ_SEC_NONE, 0, 0);
    188   obj_reloc(ob, text, 1, R_PC32, exit_sym, -4);
    189   obj_finalize(ob);
    190   return ob;
    191 }
    192 
    193 /* ---- objdump probe ------------------------------------------------ */
    194 
    195 static int have_mingw_objdump(void) {
    196   FILE* fp = popen("command -v x86_64-w64-mingw32-objdump 2>/dev/null", "r");
    197   if (!fp) return 0;
    198   char buf[256];
    199   size_t n = fread(buf, 1, sizeof buf - 1, fp);
    200   pclose(fp);
    201   return n > 0;
    202 }
    203 
    204 /* Run a shell command and slurp its stdout into a fresh malloc'd
    205  * NUL-terminated string. Returns NULL on failure. */
    206 static char* slurp_cmd(const char* cmd) {
    207   FILE* fp = popen(cmd, "r");
    208   if (!fp) return NULL;
    209   size_t cap = 4096, len = 0;
    210   char* buf = (char*)malloc(cap);
    211   if (!buf) {
    212     pclose(fp);
    213     return NULL;
    214   }
    215   for (;;) {
    216     if (len + 1024 + 1 > cap) {
    217       cap *= 2;
    218       char* nb = (char*)realloc(buf, cap);
    219       if (!nb) {
    220         free(buf);
    221         pclose(fp);
    222         return NULL;
    223       }
    224       buf = nb;
    225     }
    226     size_t got = fread(buf + len, 1, 1024, fp);
    227     len += got;
    228     if (got < 1024) break;
    229   }
    230   int rc = pclose(fp);
    231   (void)rc;
    232   buf[len] = '\0';
    233   return buf;
    234 }
    235 
    236 /* ---- main ---------------------------------------------------------- */
    237 
    238 int main(void) {
    239   if (!have_mingw_objdump()) {
    240     fprintf(stderr, "SKIP: x86_64-w64-mingw32-objdump not on PATH\n");
    241     return 0;
    242   }
    243 
    244   KitTargetSpec t;
    245   target_x64_windows(&t);
    246   Compiler* c = make_compiler(&t);
    247   if (!c) {
    248     fprintf(stderr, "FAIL: compiler_new\n");
    249     return 1;
    250   }
    251   if (setjmp(c->panic)) {
    252     fprintf(stderr, "FAIL: panic during pe-import-smoke\n");
    253     compiler_run_cleanups(c);
    254     free_compiler(c);
    255     return 1;
    256   }
    257 
    258   /* 1. Program ObjBuilder. */
    259   ObjBuilder* prog = build_program(c);
    260 
    261   /* 2. Short-import shim bytes. */
    262   uint8_t shim[SHIM_TOTAL_LEN];
    263   build_short_import_amd64(shim);
    264 
    265   /* 3. Drive the linker. */
    266   Linker* l = link_new(c);
    267   EXPECT(l != NULL, "link_new returned NULL");
    268   link_add_obj(l, prog);
    269   LinkInputId dso_id =
    270       link_add_obj_bytes(l, "ExitProcess.lib-member", shim, SHIM_TOTAL_LEN);
    271   EXPECT(dso_id != LINK_INPUT_NONE,
    272          "link_add_obj_bytes returned LINK_INPUT_NONE for short-import shim");
    273   link_set_entry(l, KIT_SLICE_LIT("mainCRTStartup"));
    274   link_set_pie(l, 1);
    275   link_set_emit_static_exe(l, 1);
    276 
    277   LinkImage* img = link_resolve(l);
    278   EXPECT(img != NULL, "link_resolve returned NULL");
    279   if (!img) {
    280     link_free(l);
    281     free_compiler(c);
    282     return 1;
    283   }
    284 
    285   /* Sanity: walk LinkSyms and find ExitProcess. The globals hashmap only
    286    * holds defined symbols, so link_symbol_lookup can't find an imported
    287    * undef by name — iterate the dense LinkSyms array instead. */
    288   {
    289     Sym exit_name = pool_intern_slice(c->global, SLICE_LIT(SHIM_SYM_CSTR));
    290     const LinkSymbol* found = NULL;
    291     /* link_symbol returns NULL once we walk off the end. */
    292     for (LinkSymId i = 1;; ++i) {
    293       const LinkSymbol* s = link_symbol(img, i);
    294       if (!s) break;
    295       if (s->name == exit_name) {
    296         found = s;
    297         break;
    298       }
    299     }
    300     EXPECT(found != NULL,
    301            "ExitProcess LinkSymbol not present after link_resolve");
    302     if (found) {
    303       EXPECT(found->imported,
    304              "ExitProcess.imported=0 (expected 1 after DSO match)");
    305       EXPECT(found->dso_input_id == dso_id,
    306              "ExitProcess.dso_input_id=%u (expected %u)",
    307              (unsigned)found->dso_input_id, (unsigned)dso_id);
    308     }
    309   }
    310 
    311   /* 4. Emit the PE. */
    312   KitWriter* w = NULL;
    313   if (kit_writer_mem(&g_heap, &w) != KIT_OK || !w) {
    314     fprintf(stderr, "FAIL: kit_writer_mem\n");
    315     link_image_free(img);
    316     link_free(l);
    317     free_compiler(c);
    318     return 1;
    319   }
    320   link_emit_image_writer(img, w);
    321 
    322   size_t out_len = 0;
    323   const uint8_t* out_bytes = kit_writer_mem_bytes(w, &out_len);
    324   EXPECT(out_len > 0, "link_emit_image_writer produced %zu bytes", out_len);
    325 
    326   /* 5. Write to /tmp and shell out to objdump. */
    327   const char* exe_path = "/tmp/pe-import-smoke.exe";
    328   (void)unlink(exe_path);
    329   FILE* fp = fopen(exe_path, "wb");
    330   EXPECT(fp != NULL, "fopen(%s) for write", exe_path);
    331   if (fp) {
    332     size_t wr = fwrite(out_bytes, 1, out_len, fp);
    333     EXPECT(wr == out_len, "fwrite wrote %zu / %zu", wr, out_len);
    334     fclose(fp);
    335   }
    336 
    337   kit_writer_close(w);
    338   link_image_free(img);
    339   link_free(l);
    340 
    341   /* objdump -p shows headers + import directory. */
    342   char* dump_p =
    343       slurp_cmd("x86_64-w64-mingw32-objdump -p /tmp/pe-import-smoke.exe 2>&1");
    344   EXPECT(dump_p != NULL, "slurp objdump -p");
    345   if (dump_p) {
    346     EXPECT(strstr(dump_p, "Magic\t\t\t020b") != NULL ||
    347                strstr(dump_p, "Magic\t020b") != NULL ||
    348                strstr(dump_p, "020b\t(PE32+)") != NULL ||
    349                strstr(dump_p, "PE32+") != NULL,
    350            "objdump -p: missing PE32+ magic 020b\n---\n%s\n---", dump_p);
    351     EXPECT(strstr(dump_p, "SectionAlignment") != NULL &&
    352                strstr(dump_p, "00001000") != NULL,
    353            "objdump -p: SectionAlignment 0x1000 missing");
    354     EXPECT(strstr(dump_p, "FileAlignment") != NULL &&
    355                strstr(dump_p, "00000200") != NULL,
    356            "objdump -p: FileAlignment 0x200 missing");
    357     EXPECT(strstr(dump_p, "Subsystem") != NULL,
    358            "objdump -p: Subsystem line missing");
    359     /* mingw objdump prints "Subsystem\t\t00000003\t(Windows CUI)" */
    360     EXPECT(strstr(dump_p, "Windows CUI") != NULL ||
    361                strstr(dump_p, "(Windows CUI)") != NULL,
    362            "objdump -p: Subsystem != Windows CUI\n---\n%s\n---", dump_p);
    363     /* Import directory: DLL Name: KERNEL32.dll. */
    364     EXPECT(strstr(dump_p, "DLL Name: " SHIM_DLL_CSTR) != NULL,
    365            "objdump -p: 'DLL Name: %s' not found\n---\n%s\n---", SHIM_DLL_CSTR,
    366            dump_p);
    367     /* The hint/name array prints "<hint>  <name>". Check ExitProcess
    368      * appears in the import list. */
    369     EXPECT(strstr(dump_p, SHIM_SYM_CSTR) != NULL,
    370            "objdump -p: '%s' not in import directory\n---\n%s\n---",
    371            SHIM_SYM_CSTR, dump_p);
    372     /* Data directories: IMPORT (idx 1) and IAT (idx 12) must be set.
    373      * mingw objdump prints them as
    374      *   "Entry 1 NNNNNNNN NNNNNNNN Import Directory"
    375      *   "Entry c NNNNNNNN NNNNNNNN Import Address Table Directory"
    376      * Reject "00000000 00000000" on those lines. */
    377     {
    378       const char* imp_line = strstr(dump_p, "Import Directory");
    379       EXPECT(imp_line != NULL, "objdump -p: 'Import Directory' line missing");
    380       if (imp_line) {
    381         /* Walk back to start of line. */
    382         const char* ls = imp_line;
    383         while (ls > dump_p && ls[-1] != '\n') --ls;
    384         EXPECT(strstr(ls, "00000000 00000000 [size]") == NULL &&
    385                    strstr(ls, "\t00000000\t00000000\t") == NULL,
    386                "Import Directory data-dir entry is zero\nline: %.*s",
    387                (int)(imp_line - ls + (int)strlen("Import Directory")), ls);
    388       }
    389       const char* iat_line = strstr(dump_p, "Import Address Table Directory");
    390       EXPECT(iat_line != NULL,
    391              "objdump -p: 'Import Address Table Directory' line missing");
    392       if (iat_line) {
    393         const char* ls = iat_line;
    394         while (ls > dump_p && ls[-1] != '\n') --ls;
    395         EXPECT(strstr(ls, "00000000 00000000 [size]") == NULL &&
    396                    strstr(ls, "\t00000000\t00000000\t") == NULL,
    397                "IAT data-dir entry is zero\nline: %.*s",
    398                (int)(iat_line - ls +
    399                      (int)strlen("Import Address Table Directory")),
    400                ls);
    401       }
    402     }
    403     free(dump_p);
    404   }
    405 
    406   /* objdump -d: confirm the .text disassembly has the call (from
    407    * mainCRTStartup) plus the per-arch IAT stub `jmp *off(%rip)` that
    408    * link_emit_coff appends. The PE has no symbol table — there's no
    409    * <mainCRTStartup> label in the disassembly, just .text. */
    410   char* dump_d =
    411       slurp_cmd("x86_64-w64-mingw32-objdump -d /tmp/pe-import-smoke.exe 2>&1");
    412   EXPECT(dump_d != NULL, "slurp objdump -d");
    413   if (dump_d) {
    414     EXPECT(strstr(dump_d, "<.text>") != NULL,
    415            "objdump -d: <.text> section header missing\n---\n%s\n---", dump_d);
    416     /* The mainCRTStartup body is a `call <disp32>` at the entry. The
    417      * disp32 must have been patched away from zero by the linker —
    418      * objdump renders it as `call 0xNNNNNNNN`, never `call 0x0`. */
    419     EXPECT(strstr(dump_d, "call ") != NULL || strstr(dump_d, "callq ") != NULL,
    420            "objdump -d: no call instruction in disassembly\n---\n%s\n---",
    421            dump_d);
    422     EXPECT(strstr(dump_d, "call   0x0\n") == NULL &&
    423                strstr(dump_d, "callq  0x0\n") == NULL,
    424            "objdump -d: call target left at 0x0 (unrelocated)\n---\n%s\n---",
    425            dump_d);
    426     /* The IAT stub is the `ff 25 disp32` indirect jmp the per-arch
    427      * stub emitter appends to .text for the imported symbol. */
    428     EXPECT(strstr(dump_d, "jmp    *") != NULL ||
    429                strstr(dump_d, "jmpq   *") != NULL ||
    430                strstr(dump_d, "ff 25") != NULL,
    431            "objdump -d: no IAT stub `jmp *off(%%rip)` in .text\n---\n%s\n---",
    432            dump_d);
    433     free(dump_d);
    434   }
    435 
    436   free_compiler(c);
    437 
    438   if (g_failures) {
    439     fprintf(stderr, "FAILED %d assertion(s)\n", g_failures);
    440     return 1;
    441   }
    442   fprintf(stderr, "OK pe-import-smoke\n");
    443   return 0;
    444 }