root/usr/src/lib/krb5/plugins/kdb/db2/adb_openclose.c
/*
 * Copyright 2007 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
 *
 *      Openvision retains the copyright to derivative works of
 *      this source code.  Do *NOT* create a derivative of this
 *      source code before consulting with your legal department.
 *      Do *NOT* integrate *ANY* of this source code into another
 *      product before consulting with your legal department.
 *
 *      For further information, read the top-level Openvision
 *      copyright which is contained in the top-level MIT Kerberos
 *      copyright.
 *
 * WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
 *
 */


/*
 * Copyright 1993 OpenVision Technologies, Inc., All Rights Reserved
 */

#include        <sys/file.h>
#include        <fcntl.h>
#include        <unistd.h>
#include        <k5-int.h>
#include        "policy_db.h"
#include        <stdlib.h>
#include        <db.h>

#define MAX_LOCK_TRIES 5

struct _locklist {
     osa_adb_lock_ent lockinfo;
     struct _locklist *next;
};

krb5_error_code osa_adb_create_db(char *filename, char *lockfilename,
                                  int magic)
{
     int lf;
     DB *db;
     BTREEINFO btinfo;

     memset(&btinfo, 0, sizeof(btinfo));
     btinfo.flags = 0;
     btinfo.cachesize = 0;
     btinfo.psize = 4096;
     btinfo.lorder = 0;
     btinfo.minkeypage = 0;
     btinfo.compare = NULL;
     btinfo.prefix = NULL;
     db = dbopen(filename, O_RDWR | O_CREAT | O_EXCL, 0600, DB_BTREE, &btinfo);
     if (db == NULL)
          return errno;
     if (db->close(db) < 0)
          return errno;

     /* only create the lock file if we successfully created the db */
     lf = THREEPARAMOPEN(lockfilename, O_RDWR | O_CREAT | O_EXCL, 0600);
     if (lf == -1)
          return errno;
     (void) close(lf);

     return OSA_ADB_OK;
}

krb5_error_code osa_adb_destroy_db(char *filename, char *lockfilename,
                                 int magic)
{
     /* the admin databases do not contain security-critical data */
     if (unlink(filename) < 0 ||
         unlink(lockfilename) < 0)
          return errno;
     return OSA_ADB_OK;
}

krb5_error_code osa_adb_rename_db(char *filefrom, char *lockfrom,
                                char *fileto, char *lockto, int magic)
{
     osa_adb_db_t fromdb, todb;
     krb5_error_code ret;

     /* make sure todb exists */
     /*LINTED*/
     if ((ret = osa_adb_create_db(fileto, lockto, magic)) &&
         ret != EEXIST)
          return ret;

     if ((ret = osa_adb_init_db(&fromdb, filefrom, lockfrom, magic)))
          return ret;
     if ((ret = osa_adb_init_db(&todb, fileto, lockto, magic))) {
          (void) osa_adb_fini_db(fromdb, magic);
          return ret;
     }
     if ((ret = osa_adb_get_lock(fromdb, KRB5_DB_LOCKMODE_PERMANENT))) {
          (void) osa_adb_fini_db(fromdb, magic);
          (void) osa_adb_fini_db(todb, magic);
          return ret;
     }
     if ((ret = osa_adb_get_lock(todb, KRB5_DB_LOCKMODE_PERMANENT))) {
          (void) osa_adb_fini_db(fromdb, magic);
          (void) osa_adb_fini_db(todb, magic);
          return ret;
     }
     if ((rename(filefrom, fileto) < 0)) {
          (void) osa_adb_fini_db(fromdb, magic);
          (void) osa_adb_fini_db(todb, magic);
          return errno;
     }
     /*
      * Do not release the lock on fromdb because it is being renamed
      * out of existence; no one can ever use it again.
      */
     if ((ret = osa_adb_release_lock(todb))) {
          (void) osa_adb_fini_db(fromdb, magic);
          (void) osa_adb_fini_db(todb, magic);
          return ret;
     }

     (void) osa_adb_fini_db(fromdb, magic);
     (void) osa_adb_fini_db(todb, magic);
     return 0;
}

krb5_error_code osa_adb_init_db(osa_adb_db_t *dbp, char *filename,
                              char *lockfilename, int magic)
{
     osa_adb_db_t db;
     static struct _locklist *locklist = NULL;
     struct _locklist *lockp;
     krb5_error_code code;

     if (dbp == NULL || filename == NULL)
          return EINVAL;

     db = (osa_adb_princ_t) malloc(sizeof(osa_adb_db_ent));
     if (db == NULL)
          return ENOMEM;

     memset(db, 0, sizeof(*db));
     db->info.hash = NULL;
     db->info.bsize = 256;
     db->info.ffactor = 8;
     db->info.nelem = 25000;
     db->info.lorder = 0;

     db->btinfo.flags = 0;
     db->btinfo.cachesize = 0;
     db->btinfo.psize = 4096;
     db->btinfo.lorder = 0;
     db->btinfo.minkeypage = 0;
     db->btinfo.compare = NULL;
     db->btinfo.prefix = NULL;
     /*
      * A process is allowed to open the same database multiple times
      * and access it via different handles.  If the handles use
      * distinct lockinfo structures, things get confused: lock(A),
      * lock(B), release(B) will result in the kernel unlocking the
      * lock file but handle A will still think the file is locked.
      * Therefore, all handles using the same lock file must share a
      * single lockinfo structure.
      *
      * It is not sufficient to have a single lockinfo structure,
      * however, because a single process may also wish to open
      * multiple different databases simultaneously, with different
      * lock files.  This code used to use a single static lockinfo
      * structure, which means that the second database opened used
      * the first database's lock file.  This was Bad.
      *
      * We now maintain a linked list of lockinfo structures, keyed by
      * lockfilename.  An entry is added when this function is called
      * with a new lockfilename, and all subsequent calls with that
      * lockfilename use the existing entry, updating the refcnt.
      * When the database is closed with fini_db(), the refcnt is
      * decremented, and when it is zero the lockinfo structure is
      * freed and reset.  The entry in the linked list, however, is
      * never removed; it will just be reinitialized the next time
      * init_db is called with the right lockfilename.
      */

     /* find or create the lockinfo structure for lockfilename */
     lockp = locklist;
     while (lockp) {
          if (strcmp(lockp->lockinfo.filename, lockfilename) == 0)
               break;
          else
               lockp = lockp->next;
     }
     if (lockp == NULL) {
          /* doesn't exist, create it, add to list */
          lockp = (struct _locklist *) malloc(sizeof(*lockp));
          if (lockp == NULL) {
               free(db);
               return ENOMEM;
          }
          memset(lockp, 0, sizeof(*lockp));
          lockp->next = locklist;
          locklist = lockp;
     }

     /* now initialize lockp->lockinfo if necessary */
     if (lockp->lockinfo.lockfile == NULL) {
          if ((code = krb5int_init_context_kdc(&lockp->lockinfo.context))) {
               free(db);
               return((krb5_error_code) code);
          }

          /*
           * needs be open read/write so that write locking can work with
           * POSIX systems
           */
          lockp->lockinfo.filename = strdup(lockfilename);
          if ((lockp->lockinfo.lockfile = fopen(lockfilename, "r+F")) == NULL) {
               /*
                * maybe someone took away write permission so we could only
                * get shared locks?
                */
               if ((lockp->lockinfo.lockfile = fopen(lockfilename, "rF"))
                   == NULL) {
                    free(db);
                    return OSA_ADB_NOLOCKFILE;
               }
          }
          lockp->lockinfo.lockmode = lockp->lockinfo.lockcnt = 0;
     }

     /* lockp is set, lockinfo is initialized, update the reference count */
     db->lock = &lockp->lockinfo;
     db->lock->refcnt++;

     db->opencnt = 0;
     db->filename = strdup(filename);
     db->magic = magic;

     *dbp = db;

     return OSA_ADB_OK;
}

krb5_error_code osa_adb_fini_db(osa_adb_db_t db, int magic)
{
     if (db->magic != magic)
          return EINVAL;
     if (db->lock->refcnt == 0) {
          /* barry says this can't happen */
          return OSA_ADB_FAILURE;
     } else {
          db->lock->refcnt--;
     }

     if (db->lock->refcnt == 0) {
          /*
           * Don't free db->lock->filename, it is used as a key to
           * find the lockinfo entry in the linked list.  If the
           * lockfile doesn't exist, we must be closing the database
           * after trashing it.  This has to be allowed, so don't
           * generate an error.
           */
          if (db->lock->lockmode != KRB5_DB_LOCKMODE_PERMANENT)
               (void) fclose(db->lock->lockfile);
          db->lock->lockfile = NULL;
          krb5_free_context(db->lock->context);
     }

     db->magic = 0;
     free(db->filename);
     free(db);
     return OSA_ADB_OK;
}

krb5_error_code osa_adb_get_lock(osa_adb_db_t db, int mode)
{
     int tries, gotlock, perm, krb5_mode, ret = 0;

     if (db->lock->lockmode >= mode) {
          /* No need to upgrade lock, just incr refcnt and return */
          db->lock->lockcnt++;
          return(OSA_ADB_OK);
     }

     perm = 0;
     switch (mode) {
        case KRB5_DB_LOCKMODE_PERMANENT:
          perm = 1;
        /* FALLTHROUGH */
        case KRB5_DB_LOCKMODE_EXCLUSIVE:
          krb5_mode = KRB5_LOCKMODE_EXCLUSIVE;
          break;
        case KRB5_DB_LOCKMODE_SHARED:
          krb5_mode = KRB5_LOCKMODE_SHARED;
          break;
        default:
          return(EINVAL);
     }

     for (gotlock = tries = 0; tries < MAX_LOCK_TRIES; tries++) {
          if ((ret = krb5_lock_file(db->lock->context,
                                    fileno(db->lock->lockfile),
                                    krb5_mode|KRB5_LOCKMODE_DONTBLOCK)) == 0) {
               gotlock++;
               break;
          } else if (ret == EBADF && mode == KRB5_DB_LOCKMODE_EXCLUSIVE)
               /* tried to exclusive-lock something we don't have */
               /* write access to */
               return OSA_ADB_NOEXCL_PERM;

          sleep(1);
     }

     /* test for all the likely "can't get lock" error codes */
     if (ret == EACCES || ret == EAGAIN || ret == EWOULDBLOCK)
          return OSA_ADB_CANTLOCK_DB;
     else if (ret != 0)
          return ret;

     /*
      * If the file no longer exists, someone acquired a permanent
      * lock.  If that process terminates its exclusive lock is lost,
      * but if we already had the file open we can (probably) lock it
      * even though it has been unlinked.  So we need to insist that
      * it exist.
      */
     if (access(db->lock->filename, F_OK) < 0) {
          (void) krb5_lock_file(db->lock->context,
                                fileno(db->lock->lockfile),
                                KRB5_LOCKMODE_UNLOCK);
          return OSA_ADB_NOLOCKFILE;
     }

     /* we have the shared/exclusive lock */

     if (perm) {
          if (unlink(db->lock->filename) < 0) {
               /* somehow we can't delete the file, but we already */
               /* have the lock, so release it and return */

               ret = errno;
               (void) krb5_lock_file(db->lock->context,
                                     fileno(db->lock->lockfile),
                                     KRB5_LOCKMODE_UNLOCK);

               /* maybe we should return CANTLOCK_DB.. but that would */
               /* look just like the db was already locked */
               return ret;
          }

          /* this releases our exclusive lock.. which is okay because */
          /* now no one else can get one either */
          (void) fclose(db->lock->lockfile);
     }

     db->lock->lockmode = mode;
     db->lock->lockcnt++;
     return OSA_ADB_OK;
}

krb5_error_code osa_adb_release_lock(osa_adb_db_t db)
{
     int ret, fd;

     if (!db->lock->lockcnt)            /* lock already unlocked */
          return OSA_ADB_NOTLOCKED;

     if (--db->lock->lockcnt == 0) {
          if (db->lock->lockmode == KRB5_DB_LOCKMODE_PERMANENT) {
               /* now we need to create the file since it does not exist */
               fd = THREEPARAMOPEN(db->lock->filename,O_RDWR | O_CREAT | O_EXCL,
                                   0600);
               if ((db->lock->lockfile = fdopen(fd, "w+F")) == NULL)
                    return OSA_ADB_NOLOCKFILE;
          } else if ((ret = krb5_lock_file(db->lock->context,
                                          fileno(db->lock->lockfile),
                                          KRB5_LOCKMODE_UNLOCK)))
               return ret;

          db->lock->lockmode = 0;
     }
     return OSA_ADB_OK;
}

krb5_error_code osa_adb_open_and_lock(osa_adb_princ_t db, int locktype)
{
     int ret;

     ret = osa_adb_get_lock(db, locktype);
     if (ret != OSA_ADB_OK)
          return ret;
     if (db->opencnt)
          goto open_ok;

     db->db = dbopen(db->filename, O_RDWR, 0600, DB_BTREE, &db->btinfo);
     if (db->db != NULL)
         goto open_ok;
     switch (errno) {
#ifdef EFTYPE
     case EFTYPE:
#endif
     case EINVAL:
          db->db = dbopen(db->filename, O_RDWR, 0600, DB_HASH, &db->info);
          if (db->db != NULL)
               goto open_ok;
          /* FALLTHROUGH */
     default:
          (void) osa_adb_release_lock(db);
          if (errno == EINVAL)
               return OSA_ADB_BAD_DB;
          return errno;
     }
open_ok:
     db->opencnt++;
     return OSA_ADB_OK;
}

krb5_error_code osa_adb_close_and_unlock(osa_adb_princ_t db)
{
     if (--db->opencnt)
          return osa_adb_release_lock(db);
     if(db->db != NULL && db->db->close(db->db) == -1) {
          (void) osa_adb_release_lock(db);
          return OSA_ADB_FAILURE;
     }

     db->db = NULL;

     return(osa_adb_release_lock(db));
}