/* Derived from Valgrind sources, coregrind/m_debuginfo/readmacho.c.
   GPL 2+ therefore.

   Can be compiled as either a 32- or 64-bit program (doesn't matter).
*/

/* What does this program do?  In short it postprocesses tool
   executables on MacOSX, after linking using /usr/bin/ld.  This is so
   as to work around a bug in the linker on Xcode 4.0.0 and Xcode
   4.0.1.  Xcode versions prior to 4.0.0 are unaffected.

   The tracking bug is https://bugs.kde.org/show_bug.cgi?id=267997

   The bug causes 64-bit tool executables to segfault at startup,
   because:

   Comparing the MachO load commands vs a (working) tool executable
   that was created by Xcode 3.2.x, it appears that the new linker has
   partially ignored the build system's request to place the tool
   executable's stack at a non standard location.  The build system
   tells the linker "-stack_addr 0x134000000 -stack_size 0x800000".

   With the Xcode 3.2 linker those flags produce two results:

   (1) A load command to allocate the stack at the said location:
          Load command 3
                cmd LC_SEGMENT_64
            cmdsize 72
            segname __UNIXSTACK
             vmaddr 0x0000000133800000
             vmsize 0x0000000000800000
            fileoff 2285568
           filesize 0
            maxprot 0x00000007
           initprot 0x00000003
             nsects 0
              flags 0x0

   (2) A request (in LC_UNIXTHREAD) to set %rsp to the correct value
       at process startup, 0x134000000.

   With Xcode 4.0.1, (1) is missing but (2) is still present.  The
   tool executable therefore starts up with %rsp pointing to unmapped
   memory and faults almost instantly.

   The workaround implemented by this program is documented in comment
   8 of bug 267997, viz:

   One really sick workaround is to observe that the executables
   contain a redundant MachO load command:

      Load command 2
            cmd LC_SEGMENT_64
        cmdsize 72
        segname __LINKEDIT
         vmaddr 0x0000000138dea000
         vmsize 0x00000000000ad000
        fileoff 2658304
       filesize 705632
        maxprot 0x00000007
       initprot 0x00000001
         nsects 0
          flags 0x0

   The described section presumably contains information intended for
   the dynamic linker, but is irrelevant because this is a statically
   linked executable.  Hence it might be possible to postprocess the
   executables after linking, to overwrite this entry with the
   information that would have been in the missing __UNIXSTACK entry.
   I tried this by hand (with a binary editor) earlier and got
   something that worked.
*/

#define DEBUGPRINTING 0

#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>


#undef PLAT_x86_darwin
#undef PLAT_amd64_darwin

#if defined(__APPLE__) && defined(__i386__)
#  define PLAT_x86_darwin 1
#elif defined(__APPLE__) && defined(__x86_64__)
#  define PLAT_amd64_darwin 1
#else
#  error "Can't be compiled on this platform"
#endif

#include <mach-o/loader.h>
#include <mach-o/nlist.h>
#include <mach-o/fat.h>
#include <mach/i386/thread_status.h>


typedef  unsigned char   UChar;
typedef    signed char   Char;
typedef           char   HChar; /* signfulness depends on host */

typedef  unsigned int    UInt;
typedef    signed int    Int;

typedef  unsigned char   Bool;
#define  True   ((Bool)1)
#define  False  ((Bool)0)

typedef  unsigned long   UWord;

typedef  UWord           SizeT;
typedef  UWord           Addr;

typedef  unsigned long long int   ULong;
typedef    signed long long int   Long;



__attribute__((noreturn))
void fail ( HChar* msg )
{
   fprintf(stderr, "fixup_macho_loadcmds: fail: %s\n", msg);
   exit(1);
}


/*------------------------------------------------------------*/
/*---                                                      ---*/
/*--- Mach-O file mapping/unmapping helpers                ---*/
/*---                                                      ---*/
/*------------------------------------------------------------*/

typedef
   struct {
      /* These two describe the entire mapped-in ("primary") image,
         fat headers, kitchen sink, whatnot: the entire file.  The
         image is mapped into img[0 .. img_szB-1]. */
      UChar* img;
      SizeT  img_szB;
      /* These two describe the Mach-O object of interest, which is
         presumably somewhere inside the primary image.
         map_image_aboard() below, which generates this info, will
         carefully check that the macho_ fields denote a section of
         memory that falls entirely inside img[0 .. img_szB-1]. */
      UChar* macho_img;
      SizeT  macho_img_szB;
   }
   ImageInfo;


Bool is_macho_object_file( const void* buf, SizeT szB )
{
   /* (JRS: the Mach-O headers might not be in this mapped data,
      because we only mapped a page for this initial check,
      or at least not very much, and what's at the start of the file
      is in general a so-called fat header.  The Mach-O object we're
      interested in could be arbitrarily far along the image, and so
      we can't assume its header will fall within this page.) */

   /* But we can say that either it's a fat object, in which case it
      begins with a fat header, or it's unadorned Mach-O, in which
      case it starts with a normal header.  At least do what checks we
      can to establish whether or not we're looking at something
      sane. */

   const struct fat_header*  fh_be = buf;
   const struct mach_header_64* mh    = buf;

   assert(buf);
   if (szB < sizeof(struct fat_header))
      return False;
   if (ntohl(fh_be->magic) == FAT_MAGIC)
      return True;

   if (szB < sizeof(struct mach_header_64))
      return False;
   if (mh->magic == MH_MAGIC_64)
      return True;

   return False;
}


/* Unmap an image mapped in by map_image_aboard. */
static void unmap_image ( /*MOD*/ImageInfo* ii )
{
   Int r;
   assert(ii->img);
   assert(ii->img_szB > 0);
   r = munmap( ii->img, ii->img_szB );
   /* Do we care if this fails?  I suppose so; it would indicate
      some fairly serious snafu with the mapping of the file. */
   assert( !r );
   memset(ii, 0, sizeof(*ii));
}


/* Map a given fat or thin object aboard, find the thin part if
   necessary, do some checks, and write details of both the fat and
   thin parts into *ii.  Returns 32 (and leaves the file unmapped) if
   the thin part is a 32 bit file.  Returns 64 if it's a 64 bit file.
   Does not return on failure.  Guarantees to return pointers to a
   valid(ish) Mach-O image if it succeeds. */
static Int map_image_aboard ( /*OUT*/ImageInfo* ii, HChar* filename )
{
   memset(ii, 0, sizeof(*ii));

   /* First off, try to map the thing in. */
   { SizeT  size;
     Int r, fd;
     struct stat stat_buf;

     r = stat(filename, &stat_buf);
     if (r)
        fail("Can't stat image (to determine its size)?!");
     size = stat_buf.st_size;

     fd = open(filename, O_RDWR, 0);
     if (fd == -1)
        fail("Can't open image for possible modification!");
     if (DEBUGPRINTING)
        printf("size %lu fd %d\n", size, fd);
     void* v = mmap ( NULL, size, PROT_READ|PROT_WRITE,
                                  MAP_FILE|MAP_SHARED, fd, 0 );
     if (v == MAP_FAILED) {
        perror("mmap failed");
        fail("Can't mmap image for possible modification!");
     }

     close(fd);

     ii->img     = (UChar*)v;
     ii->img_szB = size;
   }

   /* Now it's mapped in and we have .img and .img_szB set.  Look for
      the embedded Mach-O object.  If not findable, unmap and fail. */
   { struct fat_header*  fh_be;
     struct fat_header   fh;
     struct mach_header_64* mh;
     
     // Assume initially that we have a thin image, and update
     // these if it turns out to be fat.
     ii->macho_img     = ii->img;
     ii->macho_img_szB = ii->img_szB;

     // Check for fat header.
     if (ii->img_szB < sizeof(struct fat_header))
        fail("Invalid Mach-O file (0 too small).");

     // Fat header is always BIG-ENDIAN
     fh_be = (struct fat_header *)ii->img;
     fh.magic = ntohl(fh_be->magic);
     fh.nfat_arch = ntohl(fh_be->nfat_arch);
     if (fh.magic == FAT_MAGIC) {
        // Look for a good architecture.
        struct fat_arch *arch_be;
        struct fat_arch arch;
        Int f;
        if (ii->img_szB < sizeof(struct fat_header)
                          + fh.nfat_arch * sizeof(struct fat_arch))
           fail("Invalid Mach-O file (1 too small).");

        for (f = 0, arch_be = (struct fat_arch *)(fh_be+1); 
             f < fh.nfat_arch;
             f++, arch_be++) {
           Int cputype;
#          if defined(PLAT_x86_darwin)
           cputype = CPU_TYPE_X86;
#          elif defined(PLAT_amd64_darwin)
           cputype = CPU_TYPE_X86_64;
#          else
#            error "unknown architecture"
#          endif
           arch.cputype    = ntohl(arch_be->cputype);
           arch.cpusubtype = ntohl(arch_be->cpusubtype);
           arch.offset     = ntohl(arch_be->offset);
           arch.size       = ntohl(arch_be->size);
           if (arch.cputype == cputype) {
              if (ii->img_szB < arch.offset + arch.size)
                 fail("Invalid Mach-O file (2 too small).");
              ii->macho_img     = ii->img + arch.offset;
              ii->macho_img_szB = arch.size;
              break;
           }
        }
        if (f == fh.nfat_arch)
           fail("No acceptable architecture found in fat file.");
     }

     /* Sanity check what we found. */

     /* assured by logic above */
     assert(ii->img_szB >= sizeof(struct fat_header));

     if (ii->macho_img_szB < sizeof(struct mach_header_64))
        fail("Invalid Mach-O file (3 too small).");

     if (ii->macho_img_szB > ii->img_szB)
        fail("Invalid Mach-O file (thin bigger than fat).");

     if (ii->macho_img >= ii->img
         && ii->macho_img + ii->macho_img_szB <= ii->img + ii->img_szB) {
        /* thin entirely within fat, as expected */
     } else {
        fail("Invalid Mach-O file (thin not inside fat).");
     }

     mh = (struct mach_header_64 *)ii->macho_img;
     if (mh->magic == MH_MAGIC) {
        assert(ii->img);
        assert(ii->macho_img);
        assert(ii->img_szB > 0);
        assert(ii->macho_img_szB > 0);
        assert(ii->macho_img >= ii->img);
        assert(ii->macho_img + ii->macho_img_szB <= ii->img + ii->img_szB);
        return 32;
     }
     if (mh->magic != MH_MAGIC_64)
        fail("Invalid Mach-O file (bad magic).");

     if (ii->macho_img_szB < sizeof(struct mach_header_64) + mh->sizeofcmds)
        fail("Invalid Mach-O file (4 too small).");
   }

   assert(ii->img);
   assert(ii->macho_img);
   assert(ii->img_szB > 0);
   assert(ii->macho_img_szB > 0);
   assert(ii->macho_img >= ii->img);
   assert(ii->macho_img + ii->macho_img_szB <= ii->img + ii->img_szB);
   return 64;
}


/*------------------------------------------------------------*/
/*---                                                      ---*/
/*--- Mach-O top-level processing                          ---*/
/*---                                                      ---*/
/*------------------------------------------------------------*/

void modify_macho_loadcmds ( HChar* filename,
                             ULong  expected_stack_start,
                             ULong  expected_stack_size )
{
   ImageInfo ii;
   memset(&ii, 0, sizeof(ii));

   Int size = map_image_aboard( &ii, filename );
   if (size == 32) {
      fprintf(stderr, "fixup_macho_loadcmds:   Is 32-bit MachO file;"
              " no modifications needed.\n");
      goto out;
   }

   assert(size == 64);

   assert(ii.macho_img != NULL && ii.macho_img_szB > 0);

   /* Poke around in the Mach-O header, to find some important
      stuff.
      * the location of the __UNIXSTACK load command, if any
      * the location of the __LINKEDIT load command, if any
      * the initial RSP value as stated in the LC_UNIXTHREAD
   */

   /* The collected data */
   ULong init_rsp = 0;
   Bool  have_rsp = False;
   struct segment_command_64* seg__unixstack = NULL;
   struct segment_command_64* seg__linkedit  = NULL;

   /* Loop over the load commands and fill in the above 4 variables. */

   { struct mach_header_64 *mh = (struct mach_header_64 *)ii.macho_img;
      struct load_command *cmd;
      Int c;

      for (c = 0, cmd = (struct load_command *)(mh+1);
           c < mh->ncmds;
           c++, cmd = (struct load_command *)(cmd->cmdsize
                                              + (unsigned long)cmd)) {
         if (DEBUGPRINTING)
            printf("load cmd: offset %4lu   size %3d   kind %2d = ",
                   (unsigned long)((UChar*)cmd - (UChar*)ii.macho_img),
                   cmd->cmdsize, cmd->cmd);

         switch (cmd->cmd) {
            case LC_SEGMENT_64:
               if (DEBUGPRINTING)
                  printf("LC_SEGMENT_64");
               break;
            case LC_SYMTAB:
               if (DEBUGPRINTING)
                  printf("LC_SYMTAB");
               break;
            case LC_DYSYMTAB:
               if (DEBUGPRINTING)
                  printf("LC_DYSYMTAB");
               break;
            case LC_UUID:
               if (DEBUGPRINTING)
                  printf("LC_UUID");
               break;
            case LC_UNIXTHREAD:
               if (DEBUGPRINTING)
                  printf("LC_UNIXTHREAD");
               break;
            default:
                  printf("???");
               fail("unexpected load command in Mach header");
            break;
         }
         if (DEBUGPRINTING)
            printf("\n");

         /* Note what the stated initial RSP value is, so we can
            check it is as expected. */
         if (cmd->cmd == LC_UNIXTHREAD) {
            struct thread_command* tcmd = (struct thread_command*)cmd;
            UInt* w32s = (UInt*)( (UChar*)tcmd + sizeof(*tcmd) );
            if (DEBUGPRINTING)
               printf("UnixThread: flavor %u = ", w32s[0]);
            if (w32s[0] == x86_THREAD_STATE64 && !have_rsp) {
               if (DEBUGPRINTING)
                  printf("x86_THREAD_STATE64\n");
               x86_thread_state64_t* state64
                  = (x86_thread_state64_t*)(&w32s[2]);
               have_rsp = True;
               init_rsp = state64->__rsp;
               if (DEBUGPRINTING)
                  printf("rsp = 0x%llx\n", init_rsp);
            } else {
               if (DEBUGPRINTING)
                  printf("???");
            }
            if (DEBUGPRINTING)
               printf("\n");
         }

         if (cmd->cmd == LC_SEGMENT_64) {
            struct segment_command_64 *seg = (struct segment_command_64 *)cmd;
            if (0 == strcmp(seg->segname, "__LINKEDIT"))
               seg__linkedit = seg;
            if (0 == strcmp(seg->segname, "__UNIXSTACK"))
               seg__unixstack = seg;
         }

      }
   }

   /*
      Actions are then as follows:

      * (always) check the RSP value is as expected, and abort if not

      * if there's a UNIXSTACK load command, check it is as expected.
        If not abort, if yes, do nothing more.

      * (so there's no UNIXSTACK load command).  if there's a LINKEDIT
        load command, check if it is minimally usable (has 0 for
        nsects and flags).  If yes, convert it to a UNIXSTACK load
        command.  If there is none, or is unusable, then we're out of
        options and have to abort.
   */
   if (!have_rsp)
      fail("Can't find / check initial RSP setting");
   if (init_rsp != expected_stack_start + expected_stack_size)
      fail("Initial RSP value not as expected");

   fprintf(stderr, "fixup_macho_loadcmds:   "
                   "initial RSP is as expected (0x%llx)\n",
                   expected_stack_start + expected_stack_size );

   if (seg__unixstack) {
      struct segment_command_64 *seg = seg__unixstack;
      if (seg->vmaddr != expected_stack_start)
         fail("has __UNIXSTACK, but wrong ::vmaddr");
      if (seg->vmsize != expected_stack_size)
         fail("has __UNIXSTACK, but wrong ::vmsize");
      if (seg->maxprot != 7)
         fail("has __UNIXSTACK, but wrong ::maxprot (should be 7)");
      if (seg->initprot != 3)
         fail("has __UNIXSTACK, but wrong ::initprot (should be 3)");
      if (seg->nsects != 0)
         fail("has __UNIXSTACK, but wrong ::nsects (should be 0)");
      if (seg->flags != 0)
         fail("has __UNIXSTACK, but wrong ::flags (should be 0)");
      /* looks ok */
      fprintf(stderr, "fixup_macho_loadcmds:   "
              "acceptable __UNIXSTACK present; no modifications.\n" );
      goto out;
   }

   if (seg__linkedit) {
      struct segment_command_64 *seg = seg__linkedit;
      if (seg->nsects != 0)
         fail("has __LINKEDIT, but wrong ::nsects (should be 0)");
      if (seg->flags != 0)
         fail("has __LINKEDIT, but wrong ::flags (should be 0)");
      fprintf(stderr, "fixup_macho_loadcmds:   "
              "no __UNIXSTACK present.\n" );
      fprintf(stderr, "fixup_macho_loadcmds:   "
              "converting __LINKEDIT to __UNIXSTACK.\n" );
      strcpy(seg->segname, "__UNIXSTACK");
      seg->vmaddr   = expected_stack_start;
      seg->vmsize   = expected_stack_size;
      seg->fileoff  = 0;
      seg->filesize = 0;
      seg->maxprot  = 7;
      seg->initprot = 3;
      /* success */
      goto out;
   }

   /* out of options */
   fail("no __UNIXSTACK found and no usable __LINKEDIT found; "
        "out of options.");
   /* NOTREACHED */

  out:
   if (ii.img)
      unmap_image(&ii);
}


static Bool is_plausible_tool_exe_name ( HChar* nm )
{
   HChar* p;
   if (!nm)
      return False;

   // Does it end with this string?
   p = strstr(nm, "-x86-darwin");
   if (p && 0 == strcmp(p, "-x86-darwin"))
      return True;

   p = strstr(nm, "-amd64-darwin");
   if (p && 0 == strcmp(p, "-amd64-darwin"))
      return True;

   return False;
}


int main ( int argc, char** argv )
{
   Int   r;
   ULong req_stack_addr = 0;
   ULong req_stack_size = 0;

   if (argc != 4)
      fail("args: -stack_addr-arg -stack_size-arg "
           "name-of-tool-executable-to-modify"); 

   r= sscanf(argv[1], "0x%llx", &req_stack_addr);
   if (r != 1) fail("invalid stack_addr arg");

   r= sscanf(argv[2], "0x%llx", &req_stack_size);
   if (r != 1) fail("invalid stack_size arg");

   fprintf(stderr, "fixup_macho_loadcmds: "
           "requested stack_addr (top) 0x%llx, "
           "stack_size 0x%llx\n", req_stack_addr, req_stack_size );

   if (!is_plausible_tool_exe_name(argv[3]))
      fail("implausible tool exe name -- not of the form *-{x86,amd64}-darwin");

   fprintf(stderr, "fixup_macho_loadcmds: examining tool exe: %s\n", 
           argv[3] );
   modify_macho_loadcmds( argv[3], req_stack_addr - req_stack_size,
                          req_stack_size );

   return 0;
}

/*
      cmd LC_SEGMENT_64
  cmdsize 72
  segname __LINKEDIT
   vmaddr 0x0000000138dea000
   vmsize 0x00000000000ad000
  fileoff 2658304
 filesize 705632
  maxprot 0x00000007
 initprot 0x00000001
   nsects 0
    flags 0x0
*/

/*
      cmd LC_SEGMENT_64
  cmdsize 72
  segname __UNIXSTACK
   vmaddr 0x0000000133800000
   vmsize 0x0000000000800000
  fileoff 2498560
 filesize 0
  maxprot 0x00000007
 initprot 0x00000003
   nsects 0
    flags 0x0
*/