/*****************************************************************************
*
* 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 <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <limits.h>
#include <time.h>
#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<max; mom+=teste<<1) SET (feld, mom);
}
}
/******************************************************
* A prime wasn't found in the maximum search range.
* Just return the original number.
******************************************************/
free(feld);
return number;
}
/*************************************************************
* Open the file that we'll mmap in as the shared memory
* segment. If something fails, return NULL.
**************************************************************/
void *cache_alloc_mm(unsigned int bytes) {
int file_fd;
int rc;
int chunk_count;
char null_buff[1024];
size_t mm_file_len;
mm.bytes = bytes;
mm_file_len = strlen(run_path) + sizeof(CACHE_MMAP_FILE) + 1;
if (!(mm.file =
(char *)malloc(mm_file_len))) {
logger(L_ERR, L_FUNC, "could not allocate memory");
return NULL;
}
strlcpy(mm.file, run_path, mm_file_len);
strlcat(mm.file, CACHE_MMAP_FILE, mm_file_len);
if ((file_fd =
open(mm.file, O_RDWR|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR)) < 0) {
rc = errno;
logger(L_ERR, L_FUNC, "could not open mmap file: %s", mm.file);
logger(L_ERR, L_FUNC, "open: %s", strerror(rc));
return NULL;
}
memset(null_buff, 0, sizeof(null_buff));
chunk_count = (bytes / sizeof(null_buff)) + 1;
while (chunk_count > 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<table_size; x++) {
rwlock = lock.rwlock + x;
pthread_rwlock_destroy(rwlock);
}
free(lock.rwlock);
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 the current thread.
* __RWLock Impl__
**************************************************************/
int cache_get_wlock(unsigned int slot) {
int rc = 0;
if (flags & VERBOSE)
logger(L_DEBUG, L_FUNC, "attempting a write lock on slot: %d", slot);
rc = pthread_rwlock_wrlock(lock.rwlock + slot);
if (rc != 0) {
logger(L_ERR, L_FUNC, "could not acquire a write lock on slot: %d\n", slot);
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 the current thread.
* __RWLock Impl__
**************************************************************/
int cache_get_rlock(unsigned int slot) {
int rc = 0;
if (flags & VERBOSE)
logger(L_DEBUG, L_FUNC, "attempting a read lock on slot: %d", slot);
rc = pthread_rwlock_rdlock(lock.rwlock + slot);
if (rc != 0) {
logger(L_ERR, L_FUNC, "could not acquire a read lock on slot: %d\n", slot);
return -1;
}
return 0;
}
/*************************************************************
* Releases a previously acquired lock on a slot.
* __RWLock Impl__
**************************************************************/
int cache_un_lock(unsigned int slot) {
int rc = 0;
if (flags & VERBOSE)
logger(L_DEBUG, L_FUNC, "attempting to release lock on slot: %d", slot);
rc = pthread_rwlock_unlock(lock.rwlock + slot);
if (rc != 0) {
logger(L_ERR, L_FUNC, "could not release lock on slot: %d\n", slot);
return -1;
}
return 0;
}
#endif /* CACHE_USE_PTHREAD_RWLOCK */
/***************************************************************************************/
/***************************************************************************************/