/***************************************************************************** * * cache.c * * Description: Implements a credentail caching layer to ease the loading * on the authentication mechanisms. * * Copyright (C) 2003 Jeremy Rumpf * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED ``AS IS''. ANY EXPRESS OR IMPLIED WARRANTIES, * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL JEREMY RUMPF OR ANY CONTRIBUTER TO THIS SOFTWARE BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE * * Jeremy Rumpf * jrumpf@heavyload.net * *****************************************************************************/ /**************************************** * includes *****************************************/ #include "saslauthd.h" #include #include #include #include #include #include #include #include #include #include #include #include "cache.h" #include "utils.h" #include "globals.h" #include "md5global.h" #include "saslauthd_md5.h" /**************************************** * module globals *****************************************/ static struct mm_ctl mm; static struct lock_ctl lock; static struct bucket *table = NULL; static struct stats *table_stats = NULL; static unsigned int table_size = 0; static unsigned int table_timeout = 0; /**************************************** * flags global from saslauthd-main.c * run_path global from saslauthd-main.c * tx_rec() function from utils.c * logger() function from utils.c *****************************************/ /************************************************************* * The initialization function. This function will setup * the hash table's memory region, initialize the table, etc. **************************************************************/ int cache_init(void) { int bytes; char cache_magic[64]; void *base; if (!(flags & CACHE_ENABLED)) return 0; memset(cache_magic, 0, sizeof(cache_magic)); strlcpy(cache_magic, CACHE_CACHE_MAGIC, sizeof(cache_magic)); /************************************************************** * Compute the size of the hash table. This and a stats * struct will make up the memory region. **************************************************************/ if (table_size == 0) table_size = CACHE_DEFAULT_TABLE_SIZE; bytes = (table_size * CACHE_MAX_BUCKETS_PER * sizeof(struct bucket)) \ + sizeof(struct stats) + 256; if ((base = cache_alloc_mm(bytes)) == NULL) return -1; if (table_timeout == 0) table_timeout = CACHE_DEFAULT_TIMEOUT; if (flags & VERBOSE) { logger(L_DEBUG, L_FUNC, "bucket size: %d bytes", sizeof(struct bucket)); logger(L_DEBUG, L_FUNC, "stats size : %d bytes", sizeof(struct stats)); logger(L_DEBUG, L_FUNC, "timeout : %d seconds", table_timeout); logger(L_DEBUG, L_FUNC, "cache table: %d total bytes", bytes); logger(L_DEBUG, L_FUNC, "cache table: %d slots", table_size); logger(L_DEBUG, L_FUNC, "cache table: %d buckets", table_size * CACHE_MAX_BUCKETS_PER); } /************************************************************** * At the top of the region is the magic and stats struct. The * slots follow. Due to locking, the counters in the stats * struct will not be entirely accurate. **************************************************************/ memset(base, 0, bytes); memcpy(base, cache_magic, 64); table_stats = (void *)((char *)base + 64); table_stats->table_size = table_size; table_stats->max_buckets_per = CACHE_MAX_BUCKETS_PER; table_stats->sizeof_bucket = sizeof(struct bucket); table_stats->timeout = table_timeout; table_stats->bytes = bytes; table = (void *)((char *)table_stats + 128); /************************************************************** * Last, initialize the hash table locking. **************************************************************/ if (cache_init_lock() != 0) return -1; return 0; } /************************************************************* * Here we'll take some credentials and run them through * the hash table. If we have a valid hit then all is good * return CACHE_OK. If we don't get a hit, write the entry to * the result pointer and expect a later call to * cache_commit() to flush the bucket into the table. **************************************************************/ int cache_lookup(const char *user, const char *realm, const char *service, const char *password, struct cache_result *result) { int user_length = 0; int realm_length = 0; int service_length = 0; int hash_offset; unsigned char pwd_digest[16]; MD5_CTX md5_context; time_t epoch; time_t epoch_timeout; struct bucket *ref_bucket; struct bucket *low_bucket; struct bucket *high_bucket; struct bucket *read_bucket = NULL; char userrealmserv[CACHE_MAX_CREDS_LENGTH]; static char *debug = "[login=%s] [service=%s] [realm=%s]: %s"; if (!(flags & CACHE_ENABLED)) return CACHE_FAIL; memset((void *)result, 0, sizeof(struct cache_result)); result->status = CACHE_NO_FLUSH; /************************************************************** * Initial length checks **************************************************************/ user_length = strlen(user) + 1; realm_length = strlen(realm) + 1; service_length = strlen(service) + 1; if ((user_length + realm_length + service_length) > CACHE_MAX_CREDS_LENGTH) { return CACHE_TOO_BIG; } /************************************************************** * Any ideas on how not to call time() for every lookup? **************************************************************/ epoch = time(NULL); epoch_timeout = epoch - table_timeout; /************************************************************** * Get the offset into the hash table and the md5 sum of * the password. **************************************************************/ strlcpy(userrealmserv, user, sizeof(userrealmserv)); strlcat(userrealmserv, realm, sizeof(userrealmserv)); strlcat(userrealmserv, service, sizeof(userrealmserv)); hash_offset = cache_pjwhash(userrealmserv); _saslauthd_MD5Init(&md5_context); _saslauthd_MD5Update(&md5_context, password, strlen(password)); _saslauthd_MD5Final(pwd_digest, &md5_context); /************************************************************** * Loop through the bucket chain to try and find a hit. * * low_bucket = bucket at the start of the slot. * * high_bucket = last bucket in the slot. * * read_bucket = Contains the matched bucket if found. * Otherwise is NULL. * * Also, lock the slot first to avoid contention in the * bucket chain. * **************************************************************/ table_stats->attempts++; if (cache_get_rlock(hash_offset) != 0) { table_stats->misses++; table_stats->lock_failures++; return CACHE_FAIL; } low_bucket = table + (CACHE_MAX_BUCKETS_PER * hash_offset); high_bucket = low_bucket + CACHE_MAX_BUCKETS_PER; for (ref_bucket = low_bucket; ref_bucket < high_bucket; ref_bucket++) { if (strcmp(user, ref_bucket->creds + ref_bucket->user_offt) == 0 && \ strcmp (realm, ref_bucket->creds + ref_bucket->realm_offt) == 0 && \ strcmp(service, ref_bucket->creds + ref_bucket->service_offt) == 0) { read_bucket = ref_bucket; break; } } /************************************************************** * If we have our fish, check the password. If it's good, * release the slot (row) lock and return CACHE_OK. Else, * we'll write the entry to the result pointer. If we have a * read_bucket, then tell cache_commit() to not rescan the * chain (CACHE_FLUSH). Else, have cache_commit() determine the * best bucket to place the new entry (CACHE_FLUSH_WITH_RESCAN). **************************************************************/ if (read_bucket != NULL && read_bucket->created > epoch_timeout) { if (memcmp(pwd_digest, read_bucket->pwd_digest, 16) == 0) { if (flags & VERBOSE) logger(L_DEBUG, L_FUNC, debug, user, realm, service, "found with valid passwd"); cache_un_lock(hash_offset); table_stats->hits++; return CACHE_OK; } if (flags & VERBOSE) logger(L_DEBUG, L_FUNC, debug, user, realm, service, "found with invalid passwd, update pending"); result->status = CACHE_FLUSH; } else { if (flags & VERBOSE) logger(L_DEBUG, L_FUNC, debug, user, realm, service, "not found, update pending"); result->status = CACHE_FLUSH_WITH_RESCAN; } result->hash_offset = hash_offset; result->read_bucket = read_bucket; result->bucket.user_offt = 0; result->bucket.realm_offt = user_length; result->bucket.service_offt = user_length + realm_length; strcpy(result->bucket.creds + result->bucket.user_offt, user); strcpy(result->bucket.creds + result->bucket.realm_offt, realm); strcpy(result->bucket.creds + result->bucket.service_offt, service); memcpy(result->bucket.pwd_digest, pwd_digest, 16); result->bucket.created = epoch; cache_un_lock(hash_offset); table_stats->misses++; return CACHE_FAIL; } /************************************************************* * If it was later determined that the previous failed lookup * is ok, flush the result->bucket out to it's permanent home * in the hash table. **************************************************************/ void cache_commit(struct cache_result *result) { struct bucket *write_bucket; struct bucket *ref_bucket; struct bucket *low_bucket; struct bucket *high_bucket; if (!(flags & CACHE_ENABLED)) return; if (result->status == CACHE_NO_FLUSH) return; if (cache_get_wlock(result->hash_offset) != 0) { table_stats->lock_failures++; return; } if (result->status == CACHE_FLUSH) { write_bucket = result->read_bucket; } else { /********************************************************* * CACHE_FLUSH_WITH_RESCAN is the default action to take. * Simply traverse the slot looking for the oldest bucket * and mark it for writing. **********************************************************/ low_bucket = table + (CACHE_MAX_BUCKETS_PER * result->hash_offset); high_bucket = low_bucket + CACHE_MAX_BUCKETS_PER; write_bucket = low_bucket; for (ref_bucket = low_bucket; ref_bucket < high_bucket; ref_bucket++) { if (ref_bucket->created < write_bucket->created) write_bucket = ref_bucket; } } memcpy((void *)write_bucket, (void *)&(result->bucket), sizeof(struct bucket)); if (flags & VERBOSE) logger(L_DEBUG, L_FUNC, "lookup committed"); cache_un_lock(result->hash_offset); return; } /************************************************************* * Hashing function. Algorithm is an adaptation of Peter * Weinberger's (PJW) generic hashing algorithm, which * is based on Allen Holub's version. **************************************************************/ int cache_pjwhash(char *datum ) { const int BITS_IN_int = ( sizeof(int) * CHAR_BIT ); const int THREE_QUARTERS = ((int) ((BITS_IN_int * 3) / 4)); const int ONE_EIGHTH = ((int) (BITS_IN_int / 8)); const int HIGH_BITS = ( ~((unsigned int)(~0) >> ONE_EIGHTH )); unsigned int hash_value, i; for (hash_value = 0; *datum; ++datum) { hash_value = (hash_value << ONE_EIGHTH) + *datum; if ((i = hash_value & HIGH_BITS) != 0) hash_value = (hash_value ^ (i >> THREE_QUARTERS)) & ~HIGH_BITS; } return (hash_value % table_size); } /************************************************************* * Allow someone to set the hash table size (in kilobytes). * Since the hash table has to be prime, this won't be exact. **************************************************************/ void cache_set_table_size(const char *size) { unsigned int kilobytes; unsigned int bytes; unsigned int calc_bytes = 0; unsigned int calc_table_size = 1; kilobytes = strtol(size, (char **)NULL, 10); if (kilobytes <= 0) { logger(L_ERR, L_FUNC, "cache size must be positive and non zero"); exit(1); } bytes = kilobytes * 1024; calc_table_size = bytes / (sizeof(struct bucket) * CACHE_MAX_BUCKETS_PER); do { calc_table_size = cache_get_next_prime(calc_table_size); calc_bytes = calc_table_size * sizeof(struct bucket) * CACHE_MAX_BUCKETS_PER; } while (calc_bytes < bytes); table_size = calc_table_size; return; } /************************************************************* * Allow someone to set the table timeout (in seconds) **************************************************************/ void cache_set_timeout(const char *time) { table_timeout = strtol(time, (char **)NULL, 10); if (table_timeout <= 0) { logger(L_ERR, L_FUNC, "cache timeout must be positive"); exit(1); } return; } /************************************************************* * Find the next closest prime relative to the number given. * This is a variation of an implementation of the * Sieve of Erastothenes by Frank Pilhofer, * http://www.fpx.de/fp/Software/Sieve.html. **************************************************************/ unsigned int cache_get_next_prime(unsigned int number) { #define TEST(f,x) (*(f+((x)>>4))&(1<<(((x)&15L)>>1))) #define SET(f,x) *(f+((x)>>4))|=1<<(((x)&15)>>1) unsigned char *feld = NULL; unsigned int teste = 1; unsigned int max; unsigned int mom; unsigned int alloc; max = number + 20000; feld = malloc(alloc=(((max-=10000)>>4)+1)); if (feld == NULL) { logger(L_ERR, L_FUNC, "could not allocate memory"); exit(1); } memset(feld, 0, alloc); while ((teste += 2) < max) { if (!TEST(feld, teste)) { if (teste > number) { free(feld); return teste; } for (mom=3*teste; mom 0) { if (tx_rec(file_fd, null_buff, sizeof(null_buff)) != (ssize_t)sizeof(null_buff)) { rc = errno; logger(L_ERR, L_FUNC, "failed while writing to mmap file: %s", mm.file); close(file_fd); return NULL; } chunk_count--; } if ((mm.base = mmap(NULL, bytes, PROT_READ|PROT_WRITE, MAP_SHARED, file_fd, 0))== (void *)-1) { rc = errno; logger(L_ERR, L_FUNC, "could not mmap shared memory segment"); logger(L_ERR, L_FUNC, "mmap: %s", strerror(rc)); close(file_fd); return NULL; } close(file_fd); if (flags & VERBOSE) { logger(L_DEBUG, L_FUNC, "mmaped shared memory segment on file: %s", mm.file); } return mm.base; } /************************************************************* * When we die we may need to perform some cleanup on the * mmaped region. We assume we're the last process out here. * Otherwise, deleting the file may cause SIGBUS signals to * be generated for other processes. **************************************************************/ void cache_cleanup_mm(void) { if (mm.base != NULL) { munmap(mm.base, mm.bytes); unlink(mm.file); if (flags & VERBOSE) { logger(L_DEBUG, L_FUNC, "cache mmap file removed: %s", mm.file); } } return; } /***************************************************************** * The following is relative to the fcntl() locking method. Probably * used when the Sys IV SHM Implementation is in effect. ****************************************************************/ #ifdef CACHE_USE_FCNTL /************************************************************* * Setup the locking stuff required to implement the fcntl() * style record locking of the hash table. Return 0 if * everything is peachy, otherwise -1. * __FCNTL Impl__ **************************************************************/ int cache_init_lock(void) { int rc; size_t flock_file_len; flock_file_len = strlen(run_path) + sizeof(CACHE_FLOCK_FILE) + 1; if ((lock.flock_file = (char *)malloc(flock_file_len)) == NULL) { logger(L_ERR, L_FUNC, "could not allocate memory"); return -1; } strlcpy(lock.flock_file, run_path, flock_file_len); strlcat(lock.flock_file, CACHE_FLOCK_FILE, flock_file_len); if ((lock.flock_fd = open(lock.flock_file, O_RDWR|O_CREAT|O_TRUNC, S_IWUSR|S_IRUSR)) == -1) { rc = errno; logger(L_ERR, L_FUNC, "could not open flock file: %s", lock.flock_file); logger(L_ERR, L_FUNC, "open: %s", strerror(rc)); return -1; } if (flags & VERBOSE) logger(L_DEBUG, L_FUNC, "flock file opened at %s", lock.flock_file); return 0; } /************************************************************* * When the processes die we'll need to cleanup/delete * the flock_file. More for correctness than anything. * __FCNTL Impl__ **************************************************************/ void cache_cleanup_lock(void) { if (lock.flock_file != NULL) { unlink(lock.flock_file); if (flags & VERBOSE) logger(L_DEBUG, L_FUNC, "flock file removed: %s", lock.flock_file); } return; } /************************************************************* * Attempt to get a write lock on a slot. Return 0 if * everything went ok, return -1 if something bad happened. * This function is expected to block. * __FCNTL Impl__ **************************************************************/ int cache_get_wlock(unsigned int slot) { struct flock lock_st; int rc; lock_st.l_type = F_WRLCK; lock_st.l_start = slot; lock_st.l_whence = SEEK_SET; lock_st.l_len = 1; errno = 0; do { if (flags & VERBOSE) logger(L_DEBUG, L_FUNC, "attempting a write lock on slot: %d", slot); rc = fcntl(lock.flock_fd, F_SETLKW, &lock_st); } while (rc != 0 && errno == EINTR); if (rc != 0) { rc = errno; logger(L_ERR, L_FUNC, "could not acquire a write lock on slot: %d\n", slot); logger(L_ERR, L_FUNC, "fcntl: %s", strerror(rc)); return -1; } return 0; } /************************************************************* * Attempt to get a read lock on a slot. Return 0 if * everything went ok, return -1 if something bad happened. * This function is expected to block. * __FCNTL Impl__ **************************************************************/ int cache_get_rlock(unsigned int slot) { struct flock lock_st; int rc; lock_st.l_type = F_RDLCK; lock_st.l_start = slot; lock_st.l_whence = SEEK_SET; lock_st.l_len = 1; errno = 0; do { if (flags & VERBOSE) logger(L_DEBUG, L_FUNC, "attempting a read lock on slot: %d", slot); rc = fcntl(lock.flock_fd, F_SETLKW, &lock_st); } while (rc != 0 && errno == EINTR); if (rc != 0) { rc = errno; logger(L_ERR, L_FUNC, "could not acquire a read lock on slot: %d\n", slot); logger(L_ERR, L_FUNC, "fcntl: %s", strerror(rc)); return -1; } return 0; } /************************************************************* * Releases a previously acquired lock on a slot. * __FCNTL Impl__ **************************************************************/ int cache_un_lock(unsigned int slot) { struct flock lock_st; int rc; lock_st.l_type = F_UNLCK; lock_st.l_start = slot; lock_st.l_whence = SEEK_SET; lock_st.l_len = 1; errno = 0; do { if (flags & VERBOSE) logger(L_DEBUG, L_FUNC, "attempting to release lock on slot: %d", slot); rc = fcntl(lock.flock_fd, F_SETLKW, &lock_st); } while (rc != 0 && errno == EINTR); if (rc != 0) { rc = errno; logger(L_ERR, L_FUNC, "could not release lock on slot: %d\n", slot); logger(L_ERR, L_FUNC, "fcntl: %s", strerror(rc)); return -1; } return 0; } #endif /* CACHE_USE_FCNTL */ /********************************************************************** * The following is relative to the POSIX threads rwlock method of locking * slots in the hash table. Used when the Doors IPC is in effect, thus * -lpthreads is evident. ***********************************************************************/ #ifdef CACHE_USE_PTHREAD_RWLOCK /************************************************************* * Initialize a pthread_rwlock_t for every slot (row) in the * hash table. Return 0 if everything went ok, -1 if we bomb. * __RWLock Impl__ **************************************************************/ int cache_init_lock(void) { unsigned int x; pthread_rwlock_t *rwlock; if (!(lock.rwlock = (pthread_rwlock_t *)malloc(sizeof(pthread_rwlock_t) * table_size))) { logger(L_ERR, L_FUNC, "could not allocate memory"); return -1; } for (x = 0; x < table_size; x++) { rwlock = lock.rwlock + x; if (pthread_rwlock_init(rwlock, NULL) != 0) { logger(L_ERR, L_FUNC, "failed to initialize lock %d", x); return -1; } } if (flags & VERBOSE) logger(L_DEBUG, L_FUNC, "%d rwlocks initialized", table_size); return 0; } /************************************************************* * Destroy all of the rwlocks, free the buffer. * __RWLock Impl__ **************************************************************/ void cache_cleanup_lock(void) { unsigned int x; pthread_rwlock_t *rwlock; if(!lock.rwlock) return; for(x=0; x