/* secmem.c  -	memory allocation from a secure heap
 * Copyright (C) 1998, 1999, 2000, 2001, 2002,
 *               2003 Free Software Foundation, Inc.
 *
 * This file is part of Libgcrypt.
 *
 * Libgcrypt is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser general Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * Libgcrypt is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
 */

#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdarg.h>
#include <unistd.h>
#include <stddef.h>

#if defined(HAVE_MLOCK) || defined(HAVE_MMAP)
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#ifdef USE_CAPABILITIES
#include <sys/capability.h>
#endif
#endif

#include "ath.h"
#include "g10lib.h"
#include "secmem.h"

#if defined (MAP_ANON) && ! defined (MAP_ANONYMOUS)
#define MAP_ANONYMOUS MAP_ANON
#endif

#define DEFAULT_POOL_SIZE 16384
#define DEFAULT_PAGE_SIZE 4096

typedef struct memblock
{
  unsigned size;		/* Size of the memory available to the
				   user.  */
  int flags;			/* See below.  */
  PROPERLY_ALIGNED_TYPE aligned;
} memblock_t;

/* This flag specifies that the memory block is in use.  */
#define MB_FLAG_ACTIVE 1 << 0

/* The pool of secure memory.  */
static void *pool;

/* Size of POOL in bytes.  */
static size_t pool_size;

/* True, if the memory pool is ready for use.  May be checked in an
   atexit function.  */
static volatile int pool_okay;

/* True, if the memory pool is mmapped.  */
static volatile int pool_is_mmapped;

/* FIXME?  */
static int disable_secmem;
static int show_warning;
static int no_warning;
static int suspend_warning;

/* Stats.  */
static unsigned int cur_alloced, cur_blocks;

/* Lock protecting accesses to the memory pool.  */
static ath_mutex_t secmem_lock;

/* Convenient macros.  */
#define SECMEM_LOCK   ath_mutex_lock   (&secmem_lock)
#define SECMEM_UNLOCK ath_mutex_unlock (&secmem_lock)

/* The size of the memblock structure; this does not include the
   memory that is available to the user.  */
#define BLOCK_HEAD_SIZE \
  offsetof (memblock_t, aligned)

/* Convert an address into the according memory block structure.  */
#define ADDR_TO_BLOCK(addr) \
  (memblock_t *) ((char *) addr - BLOCK_HEAD_SIZE)

/* Check wether MB is a valid block.  */
#define BLOCK_VALID(mb) \
  (((char *) mb - (char *) pool) < pool_size)

/* Update the stats.  */
static void
stats_update (size_t add, size_t sub)
{
  if (add)
    {
      cur_alloced += add;
      cur_blocks++;
    }
  if (sub)
    {
      cur_alloced -= sub;
      cur_blocks--;
    }
}

/* Return the block following MB or NULL, if MB is the last block.  */
static memblock_t *
mb_get_next (memblock_t *mb)
{
  memblock_t *mb_next;

  mb_next = (memblock_t *) ((char *) mb + BLOCK_HEAD_SIZE + mb->size);
  
  if (! BLOCK_VALID (mb_next))
    mb_next = NULL;

  return mb_next;
}

/* Return the block preceeding MB or NULL, if MB is the first
   block.  */
static memblock_t *
mb_get_prev (memblock_t *mb)
{
  memblock_t *mb_prev, *mb_next;

  if (mb == pool)
    mb_prev = NULL;
  else
    {
      mb_prev = (memblock_t *) pool;
      while (1)
	{
	  mb_next = mb_get_next (mb_prev);
	  if (mb_next == mb)
	    break;
	  else
	    mb_prev = mb_next;
	}
    }

  return mb_prev;
}

/* If the preceeding block of MB and/or the following block of MB
   exist and are not active, merge them to form a bigger block.  */
static void
mb_merge (memblock_t *mb)
{
  memblock_t *mb_prev, *mb_next;

  mb_prev = mb_get_prev (mb);
  mb_next = mb_get_next (mb);

  if (mb_prev && (! (mb_prev->flags & MB_FLAG_ACTIVE)))
    {
      mb_prev->size += BLOCK_HEAD_SIZE + mb->size;
      mb = mb_prev;
    }
  if (mb_next && (! (mb_next->flags & MB_FLAG_ACTIVE)))
    mb->size += BLOCK_HEAD_SIZE + mb_next->size;
}

/* Return a new block, which can hold SIZE bytes.  */
static memblock_t *
mb_get_new (memblock_t *block, size_t size)
{
  memblock_t *mb, *mb_split;
  
  for (mb = block; BLOCK_VALID (mb); mb = mb_get_next (mb))
    if (! (mb->flags & MB_FLAG_ACTIVE) && mb->size >= size)
      {
	/* Found a free block.  */
	mb->flags |= MB_FLAG_ACTIVE;

	if (mb->size - size > BLOCK_HEAD_SIZE)
	  {
	    /* Split block.  */
	  
	    mb_split = (memblock_t *) (((char *) mb) + BLOCK_HEAD_SIZE + size);
	    mb_split->size = mb->size - size - BLOCK_HEAD_SIZE;
	    mb_split->flags = 0;

	    mb->size = size;

	    mb_merge (mb_split);

	  }

	break;
      }

  if (! BLOCK_VALID (mb))
    mb = NULL;

  return mb;
}

/* Print a warning message.  */
static void
print_warn (void)
{
  if (!no_warning)
    log_info (_("Warning: using insecure memory!\n"));
}

/* Lock the memory pages into core and drop privileges.  */
static void
lock_pool (void *p, size_t n)
{
#if defined(USE_CAPABILITIES) && defined(HAVE_MLOCK)
  int err;

  cap_set_proc (cap_from_text ("cap_ipc_lock+ep"));
  err = mlock (p, n);
  if (err && errno)
    err = errno;
  cap_set_proc (cap_from_text ("cap_ipc_lock+p"));

  if (err)
    {
      if (errno != EPERM
#ifdef EAGAIN	/* OpenBSD returns this */
	  && errno != EAGAIN
#endif
#ifdef ENOSYS	/* Some SCOs return this (function not implemented) */
	  && errno != ENOSYS
#endif
#ifdef ENOMEM  /* Linux might return this. */
            && errno != ENOMEM
#endif
	  )
	log_error ("can't lock memory: %s\n", strerror (err));
      show_warning = 1;
    }

#elif defined(HAVE_MLOCK)
  uid_t uid;
  int err;

  uid = getuid ();

#ifdef HAVE_BROKEN_MLOCK
  /* Under HP/UX mlock segfaults if called by non-root.  Note, we have
     noch checked whether mlock does really work under AIX where we
     also detected a broken nlock.  Note further, that using plock ()
     is not a good idea under AIX. */ 
  if (uid)
    {
      errno = EPERM;
      err = errno;
    }
  else
    {
      err = mlock (p, n);
      if (err && errno)
	err = errno;
    }
#else /* !HAVE_BROKEN_MLOCK */
  err = mlock (p, n);
  if (err && errno)
    err = errno;
#endif /* !HAVE_BROKEN_MLOCK */

  if (uid && ! geteuid ())
    {
      /* check that we really dropped the privs.
       * Note: setuid(0) should always fail */
      if (setuid (uid) || getuid () != geteuid () || !setuid (0))
	log_fatal ("failed to reset uid: %s\n", strerror (errno));
    }

  if (err)
    {
      if (errno != EPERM
#ifdef EAGAIN	/* OpenBSD returns this. */
	  && errno != EAGAIN
#endif
#ifdef ENOSYS	/* Some SCOs return this (function not implemented). */
	  && errno != ENOSYS
#endif
#ifdef ENOMEM  /* Linux might return this. */
            && errno != ENOMEM
#endif
	  )
	log_error ("can't lock memory: %s\n", strerror (err));
      show_warning = 1;
    }

#elif defined ( __QNX__ )
  /* QNX does not page at all, so the whole secure memory stuff does
   * not make much sense.  However it is still of use because it
   * wipes out the memory on a free().
   * Therefore it is sufficient to suppress the warning
   */
#elif defined (HAVE_DOSISH_SYSTEM) || defined (__CYGWIN__)
    /* It does not make sense to print such a warning, given the fact that 
     * this whole Windows !@#$% and their user base are inherently insecure
     */
#elif defined (__riscos__)
    /* no virtual memory on RISC OS, so no pages are swapped to disc,
     * besides we don't have mmap, so we don't use it! ;-)
     * But don't complain, as explained above.
     */
#else
  log_info ("Please note that you don't have secure memory on this system\n");
#endif
}

/* Initialize POOL.  */
static void
init_pool (size_t n)
{
  size_t pgsize;
  memblock_t *mb;

  pool_size = n;

  if (disable_secmem)
    log_bug ("secure memory is disabled");

#ifdef HAVE_GETPAGESIZE
  pgsize = getpagesize ();
#else
  pgsize = DEFAULT_PAGE_SIZE;
#endif

#if HAVE_MMAP
  pool_size = (pool_size + pgsize - 1) & ~(pgsize - 1);
#ifdef MAP_ANONYMOUS
  pool = mmap (0, pool_size, PROT_READ | PROT_WRITE,
	       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
#else /* map /dev/zero instead */
  {
    int fd;

    fd = open ("/dev/zero", O_RDWR);
    if (fd == -1)
      {
	log_error ("can't open /dev/zero: %s\n", strerror (errno));
	pool = (void *) -1;
      }
    else
      {
	pool = mmap (0, pool_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
      }
  }
#endif
  if (pool == (void *) -1)
    log_info ("can't mmap pool of %u bytes: %s - using malloc\n",
	      (unsigned) pool_size, strerror (errno));
  else
    {
      pool_is_mmapped = 1;
      pool_okay = 1;
    }

#endif
  if (!pool_okay)
    {
      pool = malloc (pool_size);
      if (!pool)
	log_fatal ("can't allocate memory pool of %u bytes\n",
		   (unsigned) pool_size);
      else
	pool_okay = 1;
    }

  /* Initialize first memory block.  */
  mb = (memblock_t *) pool;
  mb->size = pool_size;
  mb->flags = 0;
}

void
_gcry_secmem_set_flags (unsigned flags)
{
  int was_susp;

  SECMEM_LOCK;

  was_susp = suspend_warning;
  no_warning = flags & GCRY_SECMEM_FLAG_NO_WARNING;
  suspend_warning = flags & GCRY_SECMEM_FLAG_SUSPEND_WARNING;

  /* and now issue the warning if it is not longer suspended */
  if (was_susp && !suspend_warning && show_warning)
    {
      show_warning = 0;
      print_warn ();
    }

  SECMEM_UNLOCK;
}

unsigned
_gcry_secmem_get_flags (void)
{
  unsigned flags;

  SECMEM_LOCK;

  flags = no_warning ? GCRY_SECMEM_FLAG_NO_WARNING : 0;
  flags |= suspend_warning ? GCRY_SECMEM_FLAG_SUSPEND_WARNING : 0;

  SECMEM_UNLOCK;

  return flags;
}

/* Initialize the secure memory system.  If running with the necessary
   privileges, the secure memory pool will be locked into the core in
   order to prevent page-outs of the data.  Furthermore allocated
   secure memory will be wiped out when released.  */
void
_gcry_secmem_init (size_t n)
{
  SECMEM_LOCK;

  if (!n)
    {
#ifdef USE_CAPABILITIES
      /* drop all capabilities */
      cap_set_proc (cap_from_text ("all-eip"));

#elif !defined(HAVE_DOSISH_SYSTEM)
      uid_t uid;

      disable_secmem = 1;
      uid = getuid ();
      if (uid != geteuid ())
	{
	  if (setuid (uid) || getuid () != geteuid () || !setuid (0))
	    log_fatal ("failed to drop setuid\n");
	}
#endif
    }
  else
    {
      if (n < DEFAULT_POOL_SIZE)
	n = DEFAULT_POOL_SIZE;
      if (! pool_okay)
	{
	  init_pool (n);
	  if (! geteuid ())
	    lock_pool (pool, n);
	  else if (!no_warning)
	    log_info ("Secure memory is not locked into core\n");
	}
      else
	log_error ("Oops, secure memory pool already initialized\n");
    }

  SECMEM_UNLOCK;
}


static void *
_gcry_secmem_malloc_internal (size_t size)
{
  memblock_t *mb;

  if (!pool_okay)
    {
      log_info (_
	("operation is not possible without initialized secure memory\n"));
      exit (2);
    }
  if (show_warning && !suspend_warning)
    {
      show_warning = 0;
      print_warn ();
    }

  /* Blocks are always a multiple of 32. */
  size = ((size + 31) / 32) * 32;

  mb = mb_get_new ((memblock_t *) pool, size);
  if (mb)
    stats_update (size, 0);

  return mb ? &mb->aligned.c : NULL;
}

void *
_gcry_secmem_malloc (size_t size)
{
  void *p;

  SECMEM_LOCK;
  p = _gcry_secmem_malloc_internal (size);
  SECMEM_UNLOCK;
  
  return p;
}

static void
_gcry_secmem_free_internal (void *a)
{
  memblock_t *mb;
  int size;

  if (!a)
    return;

  mb = ADDR_TO_BLOCK (a);
  size = mb->size;

  /* This does not make much sense: probably this memory is held in the
   * cache. We do it anyway: */
#define MB_WIPE_OUT(byte) \
  memset ((memblock_t *) ((char *) mb + BLOCK_HEAD_SIZE), (byte), size);

  MB_WIPE_OUT (0xff);
  MB_WIPE_OUT (0xaa);
  MB_WIPE_OUT (0x55);
  MB_WIPE_OUT (0x00);

  stats_update (0, size);

  mb->flags &= ~MB_FLAG_ACTIVE;

  /* Update stats.  */

  mb_merge (mb);
}

/* Wipe out and release memory.  */
void
_gcry_secmem_free (void *a)
{
  SECMEM_LOCK;
  _gcry_secmem_free_internal (a);
  SECMEM_UNLOCK;
}

/* Realloc memory.  */
void *
_gcry_secmem_realloc (void *p, size_t newsize)
{
  memblock_t *mb;
  size_t size;
  void *a;

  SECMEM_LOCK;

  mb = (memblock_t *) ((char *) p - ((size_t) &((memblock_t *) 0)->aligned.c));
  size = mb->size;
  if (newsize < size)
    {
      /* It is easier to not shrink the memory.  */
      a = p;
    }
  else
    {
      a = _gcry_secmem_malloc_internal (newsize);
      if (a)
	{
	  memcpy (a, p, size);
	  memset ((char *) a + size, 0, newsize - size);
	  _gcry_secmem_free_internal (p);
	}
    }

  SECMEM_UNLOCK;

  return a;
}

int
_gcry_private_is_secure (const void *p)
{
  int ret = 0;

  SECMEM_LOCK;

  if (pool_okay && BLOCK_VALID (ADDR_TO_BLOCK (p)))
    ret = 1;

  SECMEM_UNLOCK;

  return ret;
}


/****************
 * Warning:  This code might be called by an interrupt handler
 *	     and frankly, there should really be such a handler,
 *	     to make sure that the memory is wiped out.
 *	     We hope that the OS wipes out mlocked memory after
 *	     receiving a SIGKILL - it really should do so, otherwise
 *	     there is no chance to get the secure memory cleaned.
 */
void
_gcry_secmem_term ()
{
  if (!pool_okay)
    return;

  wipememory2 (pool, 0xff, pool_size);
  wipememory2 (pool, 0xaa, pool_size);
  wipememory2 (pool, 0x55, pool_size);
  wipememory2 (pool, 0x00, pool_size);
#if HAVE_MMAP
  if (pool_is_mmapped)
    munmap (pool, pool_size);
#endif
  pool = NULL;
  pool_okay = 0;
  pool_size = 0;
}


void
_gcry_secmem_dump_stats ()
{
#if 1 
  SECMEM_LOCK;

 if (pool_okay)
    log_info ("secmem usage: %u/%lu bytes in %u blocks\n",
	      cur_alloced, (unsigned long)pool_size, cur_blocks);
  SECMEM_UNLOCK;
#else
  memblock_t *mb;
  int i;

  SECMEM_LOCK;

  for (i = 0, mb = (memblock_t *) pool;
       BLOCK_VALID (mb);
       mb = mb_get_next (mb), i++)
    log_info ("SECMEM: [%s] block: %i; size: %i\n",
	      (mb->flags & MB_FLAG_ACTIVE) ? "used" : "free",
	      i,
	      mb->size);
  SECMEM_UNLOCK;
#endif
}