/* +----------------------------------------------------------------------+ | PHP Version 7 | +----------------------------------------------------------------------+ | Copyright (c) The PHP Group | +----------------------------------------------------------------------+ | This source file is subject to version 3.01 of the PHP license, | | that is bundled with this package in the file LICENSE, and is | | available through the world-wide-web at the following url: | | http://www.php.net/license/3_01.txt | | If you did not receive a copy of the PHP license and are unable to | | obtain it through the world-wide-web, please send a note to | | license@php.net so we can mail you a copy immediately. | +----------------------------------------------------------------------+ | Author: Sascha Schumann | +----------------------------------------------------------------------+ */ #include "php.h" #ifdef HAVE_LIBMM #include #include #include #include #include #include #include "php_stdint.h" #include "php_session.h" #include "mod_mm.h" #include "SAPI.h" #ifdef ZTS # error mm is not thread-safe #endif #define PS_MM_FILE "session_mm_" /* This list holds all data associated with one session. */ typedef struct ps_sd { struct ps_sd *next; uint32_t hv; /* hash value of key */ time_t ctime; /* time of last change */ void *data; size_t datalen; /* amount of valid data */ size_t alloclen; /* amount of allocated memory for data */ char key[1]; /* inline key */ } ps_sd; typedef struct { MM *mm; ps_sd **hash; uint32_t hash_max; uint32_t hash_cnt; pid_t owner; } ps_mm; static ps_mm *ps_mm_instance = NULL; #if 0 # define ps_mm_debug(a) printf a #else # define ps_mm_debug(a) #endif static inline uint32_t ps_sd_hash(const char *data, int len) { uint32_t h; const char *e = data + len; for (h = 2166136261U; data < e; ) { h *= 16777619; h ^= *data++; } return h; } static void hash_split(ps_mm *data) { uint32_t nmax; ps_sd **nhash; ps_sd **ohash, **ehash; ps_sd *ps, *next; nmax = ((data->hash_max + 1) << 1) - 1; nhash = mm_calloc(data->mm, nmax + 1, sizeof(*data->hash)); if (!nhash) { /* no further memory to expand hash table */ return; } ehash = data->hash + data->hash_max + 1; for (ohash = data->hash; ohash < ehash; ohash++) { for (ps = *ohash; ps; ps = next) { next = ps->next; ps->next = nhash[ps->hv & nmax]; nhash[ps->hv & nmax] = ps; } } mm_free(data->mm, data->hash); data->hash = nhash; data->hash_max = nmax; } static ps_sd *ps_sd_new(ps_mm *data, const char *key) { uint32_t hv, slot; ps_sd *sd; int keylen; keylen = strlen(key); sd = mm_malloc(data->mm, sizeof(ps_sd) + keylen); if (!sd) { php_error_docref(NULL, E_WARNING, "mm_malloc failed, avail %ld, err %s", mm_available(data->mm), mm_error()); return NULL; } hv = ps_sd_hash(key, keylen); slot = hv & data->hash_max; sd->ctime = 0; sd->hv = hv; sd->data = NULL; sd->alloclen = sd->datalen = 0; memcpy(sd->key, key, keylen + 1); sd->next = data->hash[slot]; data->hash[slot] = sd; data->hash_cnt++; if (!sd->next) { if (data->hash_cnt >= data->hash_max) { hash_split(data); } } ps_mm_debug(("inserting %s(%p) into slot %d\n", key, sd, slot)); return sd; } static void ps_sd_destroy(ps_mm *data, ps_sd *sd) { uint32_t slot; slot = ps_sd_hash(sd->key, strlen(sd->key)) & data->hash_max; if (data->hash[slot] == sd) { data->hash[slot] = sd->next; } else { ps_sd *prev; /* There must be some entry before the one we want to delete */ for (prev = data->hash[slot]; prev->next != sd; prev = prev->next); prev->next = sd->next; } data->hash_cnt--; if (sd->data) { mm_free(data->mm, sd->data); } mm_free(data->mm, sd); } static ps_sd *ps_sd_lookup(ps_mm *data, const char *key, int rw) { uint32_t hv, slot; ps_sd *ret, *prev; hv = ps_sd_hash(key, strlen(key)); slot = hv & data->hash_max; for (prev = NULL, ret = data->hash[slot]; ret; prev = ret, ret = ret->next) { if (ret->hv == hv && !strcmp(ret->key, key)) { break; } } if (ret && rw && ret != data->hash[slot]) { /* Move the entry to the top of the linked list */ if (prev) { prev->next = ret->next; } ret->next = data->hash[slot]; data->hash[slot] = ret; } ps_mm_debug(("lookup(%s): ret=%p,hv=%u,slot=%d\n", key, ret, hv, slot)); return ret; } static int ps_mm_key_exists(ps_mm *data, const char *key) { ps_sd *sd; if (!key) { return FAILURE; } sd = ps_sd_lookup(data, key, 0); if (sd) { return SUCCESS; } return FAILURE; } const ps_module ps_mod_mm = { PS_MOD_SID(mm) }; #define PS_MM_DATA ps_mm *data = PS_GET_MOD_DATA() static int ps_mm_initialize(ps_mm *data, const char *path) { data->owner = getpid(); data->mm = mm_create(0, path); if (!data->mm) { return FAILURE; } data->hash_cnt = 0; data->hash_max = 511; data->hash = mm_calloc(data->mm, data->hash_max + 1, sizeof(ps_sd *)); if (!data->hash) { mm_destroy(data->mm); return FAILURE; } return SUCCESS; } static void ps_mm_destroy(ps_mm *data) { int h; ps_sd *sd, *next; /* This function is called during each module shutdown, but we must not release the shared memory pool, when an Apache child dies! */ if (data->owner != getpid()) { return; } for (h = 0; h < data->hash_max + 1; h++) { for (sd = data->hash[h]; sd; sd = next) { next = sd->next; ps_sd_destroy(data, sd); } } mm_free(data->mm, data->hash); mm_destroy(data->mm); free(data); } PHP_MINIT_FUNCTION(ps_mm) { int save_path_len = strlen(PS(save_path)); int mod_name_len = strlen(sapi_module.name); int euid_len; char *ps_mm_path, euid[30]; int ret; ps_mm_instance = calloc(sizeof(*ps_mm_instance), 1); if (!ps_mm_instance) { return FAILURE; } if (!(euid_len = slprintf(euid, sizeof(euid), "%d", geteuid()))) { free(ps_mm_instance); ps_mm_instance = NULL; return FAILURE; } /* Directory + '/' + File + Module Name + Effective UID + \0 */ ps_mm_path = emalloc(save_path_len + 1 + (sizeof(PS_MM_FILE) - 1) + mod_name_len + euid_len + 1); memcpy(ps_mm_path, PS(save_path), save_path_len); if (save_path_len && PS(save_path)[save_path_len - 1] != DEFAULT_SLASH) { ps_mm_path[save_path_len] = DEFAULT_SLASH; save_path_len++; } memcpy(ps_mm_path + save_path_len, PS_MM_FILE, sizeof(PS_MM_FILE) - 1); save_path_len += sizeof(PS_MM_FILE) - 1; memcpy(ps_mm_path + save_path_len, sapi_module.name, mod_name_len); save_path_len += mod_name_len; memcpy(ps_mm_path + save_path_len, euid, euid_len); ps_mm_path[save_path_len + euid_len] = '\0'; ret = ps_mm_initialize(ps_mm_instance, ps_mm_path); efree(ps_mm_path); if (ret != SUCCESS) { free(ps_mm_instance); ps_mm_instance = NULL; return FAILURE; } php_session_register_module(&ps_mod_mm); return SUCCESS; } PHP_MSHUTDOWN_FUNCTION(ps_mm) { if (ps_mm_instance) { ps_mm_destroy(ps_mm_instance); return SUCCESS; } return FAILURE; } PS_OPEN_FUNC(mm) { ps_mm_debug(("open: ps_mm_instance=%p\n", ps_mm_instance)); if (!ps_mm_instance) { return FAILURE; } PS_SET_MOD_DATA(ps_mm_instance); return SUCCESS; } PS_CLOSE_FUNC(mm) { PS_SET_MOD_DATA(NULL); return SUCCESS; } PS_READ_FUNC(mm) { PS_MM_DATA; ps_sd *sd; int ret = FAILURE; mm_lock(data->mm, MM_LOCK_RD); /* If there is an ID and strict mode, verify existence */ if (PS(use_strict_mode) && ps_mm_key_exists(data, key->val) == FAILURE) { /* key points to PS(id), but cannot change here. */ if (key) { efree(PS(id)); PS(id) = NULL; } PS(id) = PS(mod)->s_create_sid((void **)&data); if (!PS(id)) { return FAILURE; } if (PS(use_cookies)) { PS(send_cookie) = 1; } php_session_reset_id(); PS(session_status) = php_session_active; } sd = ps_sd_lookup(data, PS(id)->val, 0); if (sd) { *val = zend_string_init(sd->data, sd->datalen, 0); ret = SUCCESS; } mm_unlock(data->mm); return ret; } PS_WRITE_FUNC(mm) { PS_MM_DATA; ps_sd *sd; mm_lock(data->mm, MM_LOCK_RW); sd = ps_sd_lookup(data, key->val, 1); if (!sd) { sd = ps_sd_new(data, key->val); ps_mm_debug(("new entry for %s\n", key->val)); } if (sd) { if (val->len >= sd->alloclen) { if (data->mm) { mm_free(data->mm, sd->data); } sd->alloclen = val->len + 1; sd->data = mm_malloc(data->mm, sd->alloclen); if (!sd->data) { ps_sd_destroy(data, sd); php_error_docref(NULL, E_WARNING, "cannot allocate new data segment"); sd = NULL; } } if (sd) { sd->datalen = val->len; memcpy(sd->data, val->val, val->len); time(&sd->ctime); } } mm_unlock(data->mm); return sd ? SUCCESS : FAILURE; } PS_DESTROY_FUNC(mm) { PS_MM_DATA; ps_sd *sd; mm_lock(data->mm, MM_LOCK_RW); sd = ps_sd_lookup(data, key->val, 0); if (sd) { ps_sd_destroy(data, sd); } mm_unlock(data->mm); return SUCCESS; } PS_GC_FUNC(mm) { PS_MM_DATA; time_t limit; ps_sd **ohash, **ehash; ps_sd *sd, *next; *nrdels = 0; ps_mm_debug(("gc\n")); time(&limit); limit -= maxlifetime; mm_lock(data->mm, MM_LOCK_RW); ehash = data->hash + data->hash_max + 1; for (ohash = data->hash; ohash < ehash; ohash++) { for (sd = *ohash; sd; sd = next) { next = sd->next; if (sd->ctime < limit) { ps_mm_debug(("purging %s\n", sd->key)); ps_sd_destroy(data, sd); (*nrdels)++; } } } mm_unlock(data->mm); return *nrdels; } PS_CREATE_SID_FUNC(mm) { zend_string *sid; int maxfail = 3; PS_MM_DATA; do { sid = php_session_create_id((void **)&data); /* Check collision */ if (ps_mm_key_exists(data, sid->val) == SUCCESS) { if (sid) { zend_string_release_ex(sid, 0); sid = NULL; } if (!(maxfail--)) { return NULL; } } } while(!sid); return sid; } #endif