replace stgMallocBytesRWX() with our own allocator
authorSimon Marlow <simonmar@microsoft.com>
Tue, 30 May 2006 10:02:11 +0000 (10:02 +0000)
committerSimon Marlow <simonmar@microsoft.com>
Tue, 30 May 2006 10:02:11 +0000 (10:02 +0000)
See bug #738

Allocating executable memory is getting more difficult these days.  In
particular, the default SELinux policy on Fedora Core 5 disallows
making the heap (i.e. malloc()'d memory) executable, although it does
apparently allow mmap()'ing anonymous executable memory by default.

Previously, stgMallocBytesRWX() used malloc() underneath, and then
tried to make the page holding the memory executable.  This was rather
hacky and fails with Fedora Core 5.

This patch adds a mini-allocator for executable memory, based on the
block allocator.  We grab page-sized blocks and make them executable,
then allocate small objects from the page.  There's a simple free
function, that will free whole pages back to the system when they are
empty.

includes/Block.h
includes/Storage.h
rts/Adjustor.c
rts/Linker.c
rts/OSMem.h [new file with mode: 0644]
rts/RtsUtils.c
rts/RtsUtils.h
rts/Storage.c
rts/posix/OSMem.c [new file with mode: 0644]
rts/win32/OSMem.c [new file with mode: 0644]

index d1705ad..4080880 100644 (file)
@@ -85,8 +85,10 @@ typedef struct bdescr_ {
 #define BF_PINNED    4
 /* Block is part of a compacted generation */
 #define BF_COMPACTED 8
-/* Block is free, and on the free list */
+/* Block is free, and on the free list  (TODO: is this used?) */
 #define BF_FREE      16
+/* Block is executable */
+#define BF_EXEC             32
 
 /* Finding the block descriptor for a given block -------------------------- */
 
index 3a6bb2f..1886e09 100644 (file)
@@ -166,6 +166,10 @@ doYouWantToGC( void )
   return (alloc_blocks >= alloc_blocks_lim);
 }
 
+/* memory allocator for executable memory */
+extern void *allocateExec (nat bytes);
+extern void freeExec (void *p);
+
 /* -----------------------------------------------------------------------------
    Performing Garbage Collection
 
index fc4182e..4b042a1 100644 (file)
@@ -40,6 +40,7 @@ Haskell side.
 #include "Rts.h"
 #include "RtsExternal.h"
 #include "RtsUtils.h"
+#include "Storage.h"
 #include <stdlib.h>
 
 #if defined(_WIN32)
@@ -266,7 +267,7 @@ createAdjustor(int cconv, StgStablePtr hptr,
      <c>:      ff e0             jmp    %eax              # and jump to it.
                # the callee cleans up the stack
     */
-    adjustor = stgMallocBytesRWX(14);
+    adjustor = allocateExec(14);
     {
        unsigned char *const adj_code = (unsigned char *)adjustor;
        adj_code[0x00] = (unsigned char)0x58;  /* popl %eax  */
@@ -311,7 +312,7 @@ createAdjustor(int cconv, StgStablePtr hptr,
     That's (thankfully) the case here with the restricted set of 
     return types that we support.
   */
-    adjustor = stgMallocBytesRWX(17);
+    adjustor = allocateExec(17);
     {
        unsigned char *const adj_code = (unsigned char *)adjustor;
 
@@ -340,7 +341,7 @@ createAdjustor(int cconv, StgStablePtr hptr,
           
           We offload most of the work to AdjustorAsm.S.
         */
-        AdjustorStub *adjustorStub = stgMallocBytesRWX(sizeof(AdjustorStub));
+        AdjustorStub *adjustorStub = allocateExec(sizeof(AdjustorStub));
         adjustor = adjustorStub;
 
         extern void adjustorCode(void);
@@ -443,7 +444,7 @@ createAdjustor(int cconv, StgStablePtr hptr,
        }
 
        if (i < 6) {
-           adjustor = stgMallocBytesRWX(0x30);
+           adjustor = allocateExec(0x30);
 
            *(StgInt32 *)adjustor        = 0x49c1894d;
            *(StgInt32 *)(adjustor+0x4)  = 0x8948c889;
@@ -457,7 +458,7 @@ createAdjustor(int cconv, StgStablePtr hptr,
        }
        else
        {
-           adjustor = stgMallocBytesRWX(0x40);
+           adjustor = allocateExec(0x40);
 
            *(StgInt32 *)adjustor        = 0x35ff5141;
            *(StgInt32 *)(adjustor+0x4)  = 0x00000020;
@@ -504,7 +505,7 @@ createAdjustor(int cconv, StgStablePtr hptr,
      similarly, and local variables should be accessed via %fp, not %sp. In a
      nutshell: This should work! (Famous last words! :-)
   */
-    adjustor = stgMallocBytesRWX(4*(11+1));
+    adjustor = allocateExec(4*(11+1));
     {
         unsigned long *const adj_code = (unsigned long *)adjustor;
 
@@ -581,7 +582,7 @@ TODO: Depending on how much allocation overhead stgMallocBytes uses for
       4 bytes (getting rid of the nop), hence saving memory. [ccshan]
   */
     ASSERT(((StgWord64)wptr & 3) == 0);
-    adjustor = stgMallocBytesRWX(48);
+    adjustor = allocateExec(48);
     {
        StgWord64 *const code = (StgWord64 *)adjustor;
 
@@ -686,7 +687,7 @@ TODO: Depending on how much allocation overhead stgMallocBytes uses for
             */
                     // allocate space for at most 4 insns per parameter
                     // plus 14 more instructions.
-        adjustor = stgMallocBytesRWX(4 * (4*n + 14));
+        adjustor = allocateExec(4 * (4*n + 14));
         code = (unsigned*)adjustor;
         
         *code++ = 0x48000008; // b *+8
@@ -845,7 +846,7 @@ TODO: Depending on how much allocation overhead stgMallocBytes uses for
 #ifdef FUNDESCS
         adjustorStub = stgMallocBytes(sizeof(AdjustorStub), "createAdjustor");
 #else
-        adjustorStub = stgMallocBytesRWX(sizeof(AdjustorStub));
+        adjustorStub = allocateExec(sizeof(AdjustorStub));
 #endif
         adjustor = adjustorStub;
             
@@ -1088,7 +1089,7 @@ if ( *(unsigned char*)ptr != 0xe8 ) {
 #endif
  *((unsigned char*)ptr) = '\0';
 
stgFree(ptr);
freeExec(ptr);
 }
 
 
@@ -1101,7 +1102,7 @@ void
 initAdjustor(void)
 {
 #if defined(i386_HOST_ARCH) && defined(openbsd_HOST_OS)
-    obscure_ccall_ret_code_dyn = stgMallocBytesRWX(4);
+    obscure_ccall_ret_code_dyn = allocateExec(4);
     obscure_ccall_ret_code_dyn[0] = ((unsigned char *)obscure_ccall_ret_code)[0];
     obscure_ccall_ret_code_dyn[1] = ((unsigned char *)obscure_ccall_ret_code)[1];
     obscure_ccall_ret_code_dyn[2] = ((unsigned char *)obscure_ccall_ret_code)[2];
index 4aca397..3cfd2a8 100644 (file)
@@ -707,7 +707,8 @@ typedef struct _RtsSymbolVal {
       SymX(stg_interp_constr6_entry)            \
       SymX(stg_interp_constr7_entry)            \
       SymX(stg_interp_constr8_entry)            \
-      SymX(stgMallocBytesRWX)                   \
+      SymX(allocateExec)                       \
+      SymX(freeExec)                           \
       SymX(getAllocations)                      \
       SymX(revertCAFs)                          \
       SymX(RtsFlags)                            \
diff --git a/rts/OSMem.h b/rts/OSMem.h
new file mode 100644 (file)
index 0000000..417f106
--- /dev/null
@@ -0,0 +1,10 @@
+/* -----------------------------------------------------------------------------
+ *
+ * (c) The University of Glasgow 2006
+ *
+ * OS-specific memory management
+ *
+ * ---------------------------------------------------------------------------*/
+
+lnat getPageSize (void);
+void setExecutable (void *p, lnat len, rtsBool exec);
index af68905..bf02c32 100644 (file)
 #include <pthread.h>
 #endif
 
-#if defined(openbsd_HOST_OS) || defined(linux_HOST_OS) || defined(darwin_HOST_OS)
-#include <unistd.h>
-#include <sys/types.h>
-#include <sys/mman.h>
-
-/* no C99 header stdint.h on OpenBSD? */
-#if defined(openbsd_HOST_OS)
-typedef unsigned long my_uintptr_t;
-#else
-#include <stdint.h>
-typedef uintptr_t my_uintptr_t;
-#endif
-#endif
 
 #if defined(_WIN32)
 #include <windows.h>
@@ -325,42 +312,3 @@ int genericRaise(int sig) {
         return raise(sig);
 #endif
 }
-
-/* -----------------------------------------------------------------------------
-   Allocating executable memory
-   -------------------------------------------------------------------------- */
-
-/* Heavily arch-specific, I'm afraid.. */
-
-/*
- * Allocate len bytes which are readable, writable, and executable.
- *
- * ToDo: If this turns out to be a performance bottleneck, one could
- * e.g. cache the last VirtualProtect/mprotect-ed region and do
- * nothing in case of a cache hit.
- */
-void*
-stgMallocBytesRWX(int len)
-{
-  void *addr = stgMallocBytes(len, "mallocBytesRWX");
-#if defined(i386_HOST_ARCH) && defined(_WIN32)
-  /* This could be necessary for processors which distinguish between READ and
-     EXECUTE memory accesses, e.g. Itaniums. */
-  DWORD dwOldProtect = 0;
-  if (VirtualProtect (addr, len, PAGE_EXECUTE_READWRITE, &dwOldProtect) == 0) {
-    barf("mallocBytesRWX: failed to protect 0x%p; error=%lu; old protection: %lu\n",
-         addr, (unsigned long)GetLastError(), (unsigned long)dwOldProtect);
-  }
-#elif defined(openbsd_HOST_OS) || defined(linux_HOST_OS) || defined(darwin_HOST_OS)
-  /* malloced memory isn't executable by default on OpenBSD */
-  my_uintptr_t pageSize         = sysconf(_SC_PAGESIZE);
-  my_uintptr_t mask             = ~(pageSize - 1);
-  my_uintptr_t startOfFirstPage = ((my_uintptr_t)addr          ) & mask;
-  my_uintptr_t startOfLastPage  = ((my_uintptr_t)addr + len - 1) & mask;
-  my_uintptr_t size             = startOfLastPage - startOfFirstPage + pageSize;
-  if (mprotect((void*)startOfFirstPage, (size_t)size, PROT_EXEC | PROT_READ | PROT_WRITE) != 0) {
-    barf("mallocBytesRWX: failed to protect 0x%p\n", addr);
-  }
-#endif
-  return addr;
-}
index 96a5f0d..9313936 100644 (file)
@@ -16,9 +16,6 @@
 extern void *stgMallocBytes(int n, char *msg)
     GNUC3_ATTRIBUTE(__malloc__);
 
-extern void* stgMallocBytesRWX(int len)
-    GNUC3_ATTRIBUTE(__malloc__);
-
 extern void *stgReallocBytes(void *p, int n, char *msg);
 
 extern void *stgCallocBytes(int n, int m, char *msg)
index 974be45..ee860e2 100644 (file)
@@ -22,6 +22,7 @@
 #include "Storage.h"
 #include "Schedule.h"
 #include "RetainerProfile.h"   // for counting memory blocks (memInventory)
+#include "OSMem.h"
 
 #include <stdlib.h>
 #include <string.h>
@@ -968,6 +969,99 @@ calcNeeded(void)
     return needed;
 }
 
+/* ----------------------------------------------------------------------------
+   Executable memory
+
+   Executable memory must be managed separately from non-executable
+   memory.  Most OSs these days require you to jump through hoops to
+   dynamically allocate executable memory, due to various security
+   measures.
+
+   Here we provide a small memory allocator for executable memory.
+   Memory is managed with a page granularity; we allocate linearly
+   in the page, and when the page is emptied (all objects on the page
+   are free) we free the page again, not forgetting to make it
+   non-executable.
+   ------------------------------------------------------------------------- */
+
+static bdescr *exec_block;
+
+void *allocateExec (nat bytes)
+{
+    void *ret;
+    nat n;
+
+    ACQUIRE_SM_LOCK;
+
+    // round up to words.
+    n  = (bytes + sizeof(W_) + 1) / sizeof(W_);
+
+    if (n+1 > BLOCK_SIZE_W) {
+       barf("allocateExec: can't handle large objects");
+    }
+
+    if (exec_block == NULL || 
+       exec_block->free + n + 1 > exec_block->start + BLOCK_SIZE_W) {
+       bdescr *bd;
+       lnat pagesize = getPageSize();
+       bd = allocGroup(stg_max(1, pagesize / BLOCK_SIZE));
+       IF_DEBUG(gc, debugBelch("allocate exec block %p\n", bd->start));
+       bd->gen_no = 0;
+       bd->flags = BF_EXEC;
+       bd->link = exec_block;
+       if (exec_block != NULL) {
+           exec_block->u.back = bd;
+       }
+       bd->u.back = NULL;
+       setExecutable(bd->start, bd->blocks * BLOCK_SIZE, rtsTrue);
+       exec_block = bd;
+    }
+    *(exec_block->free) = n;  // store the size of this chunk
+    exec_block->gen_no += n;  // gen_no stores the number of words allocated
+    ret = exec_block->free + 1;
+    exec_block->free += n + 1;
+
+    RELEASE_SM_LOCK
+    return ret;
+}
+
+void freeExec (void *addr)
+{
+    StgPtr p = (StgPtr)addr - 1;
+    bdescr *bd = Bdescr((StgPtr)p);
+
+    if ((bd->flags & BF_EXEC) == 0) {
+       barf("freeExec: not executable");
+    }
+
+    if (*(StgPtr)p == 0) {
+       barf("freeExec: already free?");
+    }
+
+    ACQUIRE_SM_LOCK;
+
+    bd->gen_no -= *(StgPtr)p;
+    *(StgPtr)p = 0;
+
+    // Free the block if it is empty, but not if it is the block at
+    // the head of the queue.
+    if (bd->gen_no == 0 && bd != exec_block) {
+       IF_DEBUG(gc, debugBelch("free exec block %p\n", bd->start));
+       if (bd->u.back) {
+           bd->u.back->link = bd->link;
+       } else {
+           exec_block = bd->link;
+       }
+       if (bd->link) {
+           bd->link->u.back = bd->u.back;
+       }
+       setExecutable(bd->start, bd->blocks * BLOCK_SIZE, rtsFalse);
+       freeGroup(bd);
+    }
+
+    RELEASE_SM_LOCK
+}    
+
 /* -----------------------------------------------------------------------------
    Debugging
 
@@ -1048,6 +1142,11 @@ memInventory(void)
   // count the blocks allocated by the arena allocator
   total_blocks += arenaBlocks();
 
+  // count the blocks containing executable memory
+  for (bd = exec_block; bd; bd = bd->link) {
+    total_blocks += bd->blocks;
+  }
+
   /* count the blocks on the free list */
   free_blocks = countFreeList();
 
diff --git a/rts/posix/OSMem.c b/rts/posix/OSMem.c
new file mode 100644 (file)
index 0000000..6e8337b
--- /dev/null
@@ -0,0 +1,52 @@
+/* -----------------------------------------------------------------------------
+ *
+ * (c) The University of Glasgow 2006
+ *
+ * OS-specific memory management
+ *
+ * ---------------------------------------------------------------------------*/
+
+#include "Rts.h"
+#include "OSMem.h"
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/mman.h>
+
+/* no C99 header stdint.h on OpenBSD? */
+#if defined(openbsd_HOST_OS)
+typedef unsigned long my_uintptr_t;
+#else
+#include <stdint.h>
+typedef uintptr_t my_uintptr_t;
+#endif
+
+lnat getPageSize (void)
+{
+    static lnat pageSize = 0;
+    if (pageSize) {
+       return pageSize;
+    } else {
+       long ret;
+       ret = sysconf(_SC_PAGESIZE);
+       if (ret == -1) {
+           barf("getPageSize: cannot get page size");
+       }
+       return ret;
+    }
+}
+
+void setExecutable (void *p, lnat len, rtsBool exec)
+{
+    my_uintptr_t pageSize = getPageSize();
+
+    /* malloced memory isn't executable by default on OpenBSD */
+    my_uintptr_t mask             = ~(pageSize - 1);
+    my_uintptr_t startOfFirstPage = ((my_uintptr_t)p          ) & mask;
+    my_uintptr_t startOfLastPage  = ((my_uintptr_t)p + len - 1) & mask;
+    my_uintptr_t size             = startOfLastPage - startOfFirstPage + pageSize;
+    if (mprotect((void*)startOfFirstPage, (size_t)size, 
+                (exec ? PROT_EXEC : 0) | PROT_READ | PROT_WRITE) != 0) {
+       barf("makeExecutable: failed to protect 0x%p\n", p);
+    }
+}
diff --git a/rts/win32/OSMem.c b/rts/win32/OSMem.c
new file mode 100644 (file)
index 0000000..fe69a34
--- /dev/null
@@ -0,0 +1,34 @@
+/* -----------------------------------------------------------------------------
+ *
+ * (c) The University of Glasgow 2006
+ *
+ * OS-specific memory management
+ *
+ * ---------------------------------------------------------------------------*/
+
+#include <windows.h>
+
+lnat getPageSize (void)
+{
+    static lnat pagesize = 0;
+    if (pagesize) {
+       return pagesize;
+    } else {
+       SYSTEM_INFO sSysInfo;
+       GetSystemInfo(&sSysInfo);
+       pagesize = sSysInfo.dwPageSize;
+       return pagesize;
+    }
+}
+
+void setExecutable (void *p, lnat len, rtsBool exec)
+{
+    DWORD dwOldProtect = 0;
+    if (VirtualProtect (addr, len, 
+                       exec ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE, 
+                       &dwOldProtect) == 0)
+    {
+       barf("makeExecutable: failed to protect 0x%p; error=%lu; old protection: %lu\n",
+            addr, (unsigned long)GetLastError(), (unsigned long)dwOldProtect);
+    }
+}