FTL/src/shmem.c

1162 lines
35 KiB
C

/* Pi-hole: A black hole for Internet advertisements
* (c) 2018 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* FTL Engine
* Shared memory subroutines
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
#include "FTL.h"
#define SHMEM_PRIVATE
#include "shmem.h"
#include "overTime.h"
#include "log.h"
#include "config.h"
// data getter functions
#include "datastructure.h"
// get_num_regex()
#include "regex_r.h"
// NAME_MAX
#include <limits.h>
// gettid
#include "daemon.h"
// generate_backtrace()
#include "signals.h"
// get_path_usage()
#include "files.h"
// log_resource_shortage()
#include "database/message-table.h"
// check_running_FTL()
#include "procps.h"
/// The version of shared memory used
#define SHARED_MEMORY_VERSION 14
/// The name of the shared memory. Use this when connecting to the shared memory.
#define SHMEM_PATH "/dev/shm"
#define SHARED_LOCK_NAME "FTL-lock"
#define SHARED_STRINGS_NAME "FTL-strings"
#define SHARED_COUNTERS_NAME "FTL-counters"
#define SHARED_DOMAINS_NAME "FTL-domains"
#define SHARED_CLIENTS_NAME "FTL-clients"
#define SHARED_QUERIES_NAME "FTL-queries"
#define SHARED_UPSTREAMS_NAME "FTL-upstreams"
#define SHARED_OVERTIME_NAME "FTL-overTime"
#define SHARED_SETTINGS_NAME "FTL-settings"
#define SHARED_DNS_CACHE "FTL-dns-cache"
#define SHARED_PER_CLIENT_REGEX "FTL-per-client-regex"
// Allocation step for FTL-strings bucket. This is somewhat special as we use
// this as a general-purpose storage which should always be large enough. If,
// for some reason, more data than this step has to be stored (highly unlikely,
// close to impossible), the data will be properly truncated and we try again in
// the next lock round
#define STRINGS_ALLOC_STEP (10*pagesize)
// Global counters struct
countersStruct *counters = NULL;
/// The pointer in shared memory to the shared string buffer
static SharedMemory shm_lock = { 0 };
static SharedMemory shm_strings = { 0 };
static SharedMemory shm_counters = { 0 };
static SharedMemory shm_domains = { 0 };
static SharedMemory shm_clients = { 0 };
static SharedMemory shm_queries = { 0 };
static SharedMemory shm_upstreams = { 0 };
static SharedMemory shm_overTime = { 0 };
static SharedMemory shm_settings = { 0 };
static SharedMemory shm_dns_cache = { 0 };
static SharedMemory shm_per_client_regex = { 0 };
static SharedMemory *sharedMemories[] = { &shm_lock,
&shm_strings,
&shm_counters,
&shm_domains,
&shm_clients,
&shm_queries,
&shm_upstreams,
&shm_overTime,
&shm_settings,
&shm_dns_cache,
&shm_per_client_regex };
#define NUM_SHMEM (sizeof(sharedMemories)/sizeof(SharedMemory*))
// Variable size array structs
static queriesData *queries = NULL;
static clientsData *clients = NULL;
static domainsData *domains = NULL;
static upstreamsData *upstreams = NULL;
static DNSCacheData *dns_cache = NULL;
typedef struct {
struct {
pthread_mutex_t outer;
pthread_mutex_t inner;
} lock;
struct {
volatile pid_t pid;
volatile pid_t tid;
} owner;
} ShmLock;
static ShmLock *shmLock = NULL;
static ShmSettings *shmSettings = NULL;
static int pagesize;
static unsigned int local_shm_counter = 0;
static pid_t shmem_pid = 0;
static size_t used_shmem = 0u;
static size_t get_optimal_object_size(const size_t objsize, const size_t minsize);
// Private prototypes
static void *enlarge_shmem_struct(const char type);
static int get_dev_shm_usage(char buffer[64])
{
char buffer2[64] = { 0 };
const int percentage = get_path_usage(SHMEM_PATH, buffer2);
// Generate human-readable "used by FTL" size
char prefix_FTL[2] = { 0 };
double formatted_FTL = 0.0;
format_memory_size(prefix_FTL, used_shmem, &formatted_FTL);
// Print result into buffer passed to this subroutine
snprintf(buffer, 64, "%s, FTL uses %.1f%sB",
buffer2, formatted_FTL, prefix_FTL);
// Return percentage
return percentage;
}
// Verify the PID stored during shared memory initialization is the same as ours
// (while we initialized the shared memory objects)
static void verify_shmem_pid(void)
{
// Open shared memory settings object
const int settingsfd = shm_open(SHARED_SETTINGS_NAME, O_RDONLY, S_IRUSR | S_IWUSR);
if(settingsfd == -1)
{
logg("FATAL: verify_shmem_pid(): Failed to open shared memory object \"%s\": %s",
SHARED_SETTINGS_NAME, strerror(errno));
exit(EXIT_FAILURE);
}
ShmSettings shms = { 0 };
if(read(settingsfd, &shms, sizeof(shms)) != sizeof(shms))
{
logg("FATAL: verify_shmem_pid(): Failed to read %zu bytes from shared memory object \"%s\": %s",
sizeof(shms), SHARED_SETTINGS_NAME, strerror(errno));
exit(EXIT_FAILURE);
}
close(settingsfd);
// Compare the SHM's PID to the one we had when creating the SHM objects
if(shms.pid == shmem_pid)
return;
// If we reach here, we are in serious trouble. Terminating with error
// code is the most sensible thing we can do at this point
logg("FATAL: Shared memory is owned by a different process (PID %d)", shms.pid);
check_running_FTL();
logg("Exiting now!");
exit(EXIT_FAILURE);
}
// chown_shmem() changes the file ownership of a given shared memory object
static bool chown_shmem(SharedMemory *sharedMemory, struct passwd *ent_pw)
{
// Open shared memory object
const int fd = shm_open(sharedMemory->name, O_RDWR, S_IRUSR | S_IWUSR);
if(fd == -1)
{
logg("FATAL: chown_shmem(): Failed to open shared memory object \"%s\": %s",
sharedMemory->name, strerror(errno));
exit(EXIT_FAILURE);
}
if(fchown(fd, ent_pw->pw_uid, ent_pw->pw_gid) == -1)
{
logg("WARNING: chown_shmem(%d, %d, %d): failed for %s: %s (%d)",
fd, ent_pw->pw_uid, ent_pw->pw_gid, sharedMemory->name,
strerror(errno), errno);
return false;
}
logg("Changing %s (%d) to %d:%d", sharedMemory->name, fd, ent_pw->pw_uid, ent_pw->pw_gid);
// Close shared memory object file descriptor as it is no longer
// needed after having called ftruncate()
close(fd);
return true;
}
// A function that duplicates a string and replaces all characters "s" by "r"
static char *__attribute__ ((malloc)) str_replace(const char *input,
const char s,
const char r,
unsigned int *N)
{
// Duplicate string
char *copy = strdup(input);
if(!copy)
return NULL;
// Woring pointer
char *ix = copy;
// Loop over string until there are no further "s" chars in the string
while((ix = strchr(ix, s)) != NULL)
{
*ix++ = r;
(*N)++;
}
return copy;
}
char *__attribute__ ((malloc)) str_escape(const char *input, unsigned int *N)
{
// If no escaping is done, this routine returns the original pointer
// and N stays 0
*N = 0;
if(strchr(input, ' ') != NULL)
{
// Replace any spaces by ~ if we find them in the domain name
// This is necessary as our telnet API uses space delimiters
return str_replace(input, ' ', '~', N);
}
return strdup(input);
}
bool strcmp_escaped(const char *a, const char *b)
{
unsigned int Na, Nb;
// Input check
if(a == NULL || b == NULL)
return false;
// Escape both inputs
char *aa = str_escape(a, &Na);
char *bb = str_escape(b, &Nb);
// Check for memory errors
if(!aa || !bb)
{
if(aa) free(aa);
if(bb) free(bb);
return false;
}
const char result = strcasecmp(aa, bb) == 0;
free(aa);
free(bb);
return result;
}
size_t addstr(const char *input)
{
if(input == NULL)
{
logg("WARN: Called addstr() with NULL pointer");
return 0;
}
// Get string length, add terminating character
size_t len = strlen(input) + 1;
const size_t avail_mem = shm_strings.size - shmSettings->next_str_pos;
// If this is an empty string (only the terminating character is present),
// use the shared memory string at position zero instead of creating a new
// entry here. We also ensure that the given string is not too long to
// prevent possible memory corruption caused by strncpy() further down
if(len == 1)
{
return 0;
}
else if(len > (size_t)(pagesize-1))
{
logg("WARN: Shortening too long string (len %zu > pagesize %i)", len, pagesize);
len = pagesize;
}
else if(len > (size_t)(avail_mem-1))
{
logg("WARN: Shortening too long string (len %zu > available memory %zu)", len, avail_mem);
len = avail_mem;
}
unsigned int N = 0;
char *str = str_escape(input, &N);
if(N > 0)
logg("INFO: FTL replaced %u invalid characters with ~ in the query \"%s\"", N, str);
// Debugging output
if(config.debug & DEBUG_SHMEM)
logg("Adding \"%s\" (len %zu) to buffer. next_str_pos is %u", str, len, shmSettings->next_str_pos);
// Copy the C string pointed by str into the shared string buffer
strncpy(&((char*)shm_strings.ptr)[shmSettings->next_str_pos], str, len);
free(str);
// Increment string length counter
shmSettings->next_str_pos += len;
// Return start of stored string
return (shmSettings->next_str_pos - len);
}
const char *_getstr(const size_t pos, const char *func, const int line, const char *file)
{
// Only access the string memory if this memory region has already been set
if(pos < shmSettings->next_str_pos)
return &((const char*)shm_strings.ptr)[pos];
else
{
logg("WARN: Tried to access %zu in %s() (%s:%i) but next_str_pos is %u", pos, func, file, line, shmSettings->next_str_pos);
return "";
}
}
/// Create a mutex for shared memory
static pthread_mutex_t create_mutex(void) {
logg("Creating mutex");
pthread_mutexattr_t lock_attr = {};
pthread_mutex_t lock = {};
// Initialize the lock attributes
pthread_mutexattr_init(&lock_attr);
// Allow the lock to be used by other processes
pthread_mutexattr_setpshared(&lock_attr, PTHREAD_PROCESS_SHARED);
// Make the lock robust against process death
pthread_mutexattr_setrobust(&lock_attr, PTHREAD_MUTEX_ROBUST);
// Initialize the lock
pthread_mutex_init(&lock, &lock_attr);
// Destroy the lock attributes since we're done with it
pthread_mutexattr_destroy(&lock_attr);
return lock;
}
static void remap_shm(void)
{
// Remap shared object pointers which might have changed
realloc_shm(&shm_queries, counters->queries_MAX, sizeof(queriesData), false);
queries = (queriesData*)shm_queries.ptr;
realloc_shm(&shm_domains, counters->domains_MAX, sizeof(domainsData), false);
domains = (domainsData*)shm_domains.ptr;
realloc_shm(&shm_clients, counters->clients_MAX, sizeof(clientsData), false);
clients = (clientsData*)shm_clients.ptr;
realloc_shm(&shm_upstreams, counters->upstreams_MAX, sizeof(upstreamsData), false);
upstreams = (upstreamsData*)shm_upstreams.ptr;
realloc_shm(&shm_dns_cache, counters->dns_cache_MAX, sizeof(DNSCacheData), false);
dns_cache = (DNSCacheData*)shm_dns_cache.ptr;
realloc_shm(&shm_per_client_regex, counters->per_client_regex_MAX, sizeof(bool), false);
// per-client-regex bools are not exposed by a global pointer
realloc_shm(&shm_strings, counters->strings_MAX, sizeof(char), false);
// strings are not exposed by a global pointer
// Update local counter to reflect that we absorbed this change
local_shm_counter = shmSettings->global_shm_counter;
}
// Obtain SHMEM lock
void _lock_shm(const char *func, const int line, const char *file)
{
if(config.debug & DEBUG_LOCKS)
logg("Waiting for SHM lock in %s() (%s:%i)", func, file, line);
int result = pthread_mutex_lock(&shmLock->lock.outer);
if(result != 0)
logg("Error when obtaining outer SHM lock: %s", strerror(result));
if(result == EOWNERDEAD) {
// Try to make the lock consistent if the other process died while
// holding the lock
if(config.debug & DEBUG_LOCKS)
logg("Owner of outer SHM lock died, making lock consistent");
result = pthread_mutex_consistent(&shmLock->lock.outer);
if(result != 0)
logg("Failed to make outer SHM lock consistent: %s", strerror(result));
}
// Store lock owner after lock has been acquired and was made consistent (if required)
shmLock->owner.pid = getpid();
shmLock->owner.tid = gettid();
// Check if this process needs to remap the shared memory objects
if(shmSettings != NULL &&
local_shm_counter != shmSettings->global_shm_counter)
{
if(config.debug & DEBUG_SHMEM)
logg("Remapping shared memory for current process %u %u",
local_shm_counter, shmSettings->global_shm_counter);
remap_shm();
}
// Ensure we have enough shared memory available for new data
shm_ensure_size();
result = pthread_mutex_lock(&shmLock->lock.inner);
if(config.debug & DEBUG_LOCKS)
logg("Obtained SHM lock for %s() (%s:%i)", func, file, line);
if(result != 0)
logg("Error when obtaining inner SHM lock: %s", strerror(result));
if(result == EOWNERDEAD) {
// Try to make the lock consistent if the other process died while
// holding the lock
if(config.debug & DEBUG_LOCKS)
logg("Owner of inner SHM lock died, making lock consistent");
result = pthread_mutex_consistent(&shmLock->lock.inner);
if(result != 0)
logg("Failed to make inner SHM lock consistent: %s", strerror(result));
}
}
// Release SHM lock
void _unlock_shm(const char* func, const int line, const char * file)
{
if(config.debug & DEBUG_LOCKS && !is_our_lock())
{
logg("ERROR: Tried to unlock but lock is owned by %li/%li",
(long int)shmLock->owner.pid, (long int)shmLock->owner.tid);
}
// Unlock mutex
int result = pthread_mutex_unlock(&shmLock->lock.inner);
shmLock->owner.pid = 0;
shmLock->owner.tid = 0;
if(config.debug & DEBUG_LOCKS)
logg("Removed lock in %s() (%s:%i)", func, file, line);
if(result != 0)
logg("Failed to unlock inner SHM lock: %s", strerror(result));
result = pthread_mutex_unlock(&shmLock->lock.outer);
if(result != 0)
logg("Failed to unlock outer SHM lock: %s", strerror(result));
}
// Return if we locked this mutex (PID and TID match)
bool is_our_lock(void)
{
if(shmLock->owner.pid == getpid() &&
shmLock->owner.tid == gettid())
return true;
return false;
}
bool init_shmem()
{
// Get kernel's page size
pagesize = getpagesize();
/****************************** shared memory lock ******************************/
// Try to create shared memory object
shm_lock = create_shm(SHARED_LOCK_NAME, sizeof(ShmLock));
if(shm_lock.ptr == NULL)
return false;
shmLock = (ShmLock*)shm_lock.ptr;
shmLock->lock.outer = create_mutex();
shmLock->lock.inner = create_mutex();
/****************************** shared counters struct ******************************/
// Try to create shared memory object
shm_counters = create_shm(SHARED_COUNTERS_NAME, sizeof(countersStruct));
if(shm_counters.ptr == NULL)
return false;
counters = (countersStruct*)shm_counters.ptr;
/****************************** shared settings struct ******************************/
// Try to create shared memory object
shm_settings = create_shm(SHARED_SETTINGS_NAME, sizeof(ShmSettings));
if(shm_settings.ptr == NULL)
return false;
shmSettings = (ShmSettings*)shm_settings.ptr;
shmSettings->version = SHARED_MEMORY_VERSION;
shmSettings->global_shm_counter = 0;
shmSettings->pid = shmem_pid = getpid();
/****************************** shared strings buffer ******************************/
// Try to create shared memory object
shm_strings = create_shm(SHARED_STRINGS_NAME, STRINGS_ALLOC_STEP);
if(shm_strings.ptr == NULL)
return false;
counters->strings_MAX = shm_strings.size;
// Initialize shared string object with an empty string at position zero
((char*)shm_strings.ptr)[0] = '\0';
shmSettings->next_str_pos = 1;
/****************************** shared domains struct ******************************/
size_t size = get_optimal_object_size(sizeof(domainsData), 1);
// Try to create shared memory object
shm_domains = create_shm(SHARED_DOMAINS_NAME, size*sizeof(domainsData));
if(shm_domains.ptr == NULL)
return false;
domains = (domainsData*)shm_domains.ptr;
counters->domains_MAX = size;
/****************************** shared clients struct ******************************/
size = get_optimal_object_size(sizeof(clientsData), 1);
// Try to create shared memory object
shm_clients = create_shm(SHARED_CLIENTS_NAME, size*sizeof(clientsData));
if(shm_clients.ptr == NULL)
return false;
clients = (clientsData*)shm_clients.ptr;
counters->clients_MAX = size;
/****************************** shared upstreams struct ******************************/
size = get_optimal_object_size(sizeof(upstreamsData), 1);
// Try to create shared memory object
shm_upstreams = create_shm(SHARED_UPSTREAMS_NAME, size*sizeof(upstreamsData));
if(shm_upstreams.ptr == NULL)
return false;
upstreams = (upstreamsData*)shm_upstreams.ptr;
counters->upstreams_MAX = size;
/****************************** shared queries struct ******************************/
// Try to create shared memory object
shm_queries = create_shm(SHARED_QUERIES_NAME, pagesize*sizeof(queriesData));
if(shm_queries.ptr == NULL)
return false;
queries = (queriesData*)shm_queries.ptr;
counters->queries_MAX = pagesize;
/****************************** shared overTime struct ******************************/
size = get_optimal_object_size(sizeof(overTimeData), OVERTIME_SLOTS);
// Try to create shared memory object
shm_overTime = create_shm(SHARED_OVERTIME_NAME, size*sizeof(overTimeData));
if(shm_overTime.ptr == NULL)
return false;
// set global pointer in overTime.c
overTime = (overTimeData*)shm_overTime.ptr;
/****************************** shared DNS cache struct ******************************/
size = get_optimal_object_size(sizeof(DNSCacheData), 1);
// Try to create shared memory object
shm_dns_cache = create_shm(SHARED_DNS_CACHE, size*sizeof(DNSCacheData));
if(shm_dns_cache.ptr == NULL)
return false;
dns_cache = (DNSCacheData*)shm_dns_cache.ptr;
counters->dns_cache_MAX = size;
/****************************** shared per-client regex buffer ******************************/
size = pagesize; // Allocate one pagesize initially. This may be expanded later on
// Try to create shared memory object
shm_per_client_regex = create_shm(SHARED_PER_CLIENT_REGEX, size);
if(shm_per_client_regex.ptr == NULL)
return false;
counters->per_client_regex_MAX = size;
return true;
}
// CHOWN all shared memory objects to supplied user/group
void chown_all_shmem(struct passwd *ent_pw)
{
for(unsigned int i = 0; i < NUM_SHMEM; i++)
chown_shmem(sharedMemories[i], ent_pw);
}
// Destroy mutex and, subsequently, delete all shared memory objects
void destroy_shmem(void)
{
// First, we destroy the mutex
if(shmLock != NULL)
{
pthread_mutex_destroy(&shmLock->lock.inner);
pthread_mutex_destroy(&shmLock->lock.outer);
}
shmLock = NULL;
// Then, we delete the shared memory objects
for(unsigned int i = 0; i < NUM_SHMEM; i++)
delete_shm(sharedMemories[i]);
}
/// Create shared memory
///
/// \param name the name of the shared memory
/// \param size the size to allocate
/// \return a structure with a pointer to the mounted shared memory. The pointer
/// will always be valid, because if it failed FTL will have exited.
static SharedMemory create_shm(const char *name, const size_t size)
{
char df[64] = { 0 };
const int percentage = get_dev_shm_usage(df);
if(config.debug & DEBUG_SHMEM || (config.check.shmem > 0 && percentage > config.check.shmem))
{
logg("Creating shared memory with name \"%s\" and size %zu (%s)", name, size, df);
}
if(config.check.shmem > 0 && percentage > config.check.shmem)
log_resource_shortage(-1.0, 0, percentage, -1, SHMEM_PATH, df);
SharedMemory sharedMemory = {
.name = name,
.size = size,
.ptr = NULL
};
// Create the shared memory file in read/write mode with 600 (u+rw) permissions
// and the following open flags:
// - O_RDWR: Open the object for read-write access (we need to be able to modify the locks)
// - O_CREAT: Create the shared memory object if it does not exist.
// - O_EXCL: Return an error if a shared memory object with the given name already exists.
errno = 0;
const int fd = shm_open(sharedMemory.name, O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
// Check for `shm_open` error
if(fd == -1)
{
logg("FATAL: create_shm(): Failed to create shared memory object \"%s\": %s",
name, strerror(errno));
return sharedMemory;
}
// Allocate shared memory object to specified size
// Using f[tl]allocate() will ensure that there's actually space for
// this file. Otherwise we end up with a sparse file that can give
// SIGBUS if we run out of space while writing to it.
const int ret = ftlallocate(fd, 0U, size);
if(ret != 0)
{
logg("FATAL: create_shm(): Failed to resize \"%s\" (%i) to %zu: %s (%i)",
sharedMemory.name, fd, size, strerror(errno), ret);
exit(EXIT_FAILURE);
}
// Update how much memory FTL uses
// We only add here as this is a new file
used_shmem += size;
// Create shared memory mapping
void *shm = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// Check for `mmap` error
if(shm == MAP_FAILED)
{
logg("FATAL: create_shm(): Failed to map shared memory object \"%s\" (%i): %s",
sharedMemory.name, fd, strerror(errno));
return sharedMemory;
}
// Close shared memory object file descriptor as it is no longer
// needed after having called mmap()
close(fd);
sharedMemory.ptr = shm;
return sharedMemory;
}
static void *enlarge_shmem_struct(const char type)
{
SharedMemory *sharedMemory = NULL;
size_t sizeofobj, allocation_step;
int *counter = NULL;
// Select type of struct that should be enlarged
switch(type)
{
case QUERIES:
sharedMemory = &shm_queries;
allocation_step = pagesize;
sizeofobj = sizeof(queriesData);
counter = &counters->queries_MAX;
break;
case CLIENTS:
sharedMemory = &shm_clients;
allocation_step = get_optimal_object_size(sizeof(clientsData), 1);
sizeofobj = sizeof(clientsData);
counter = &counters->clients_MAX;
break;
case DOMAINS:
sharedMemory = &shm_domains;
allocation_step = get_optimal_object_size(sizeof(domainsData), 1);
sizeofobj = sizeof(domainsData);
counter = &counters->domains_MAX;
break;
case UPSTREAMS:
sharedMemory = &shm_upstreams;
allocation_step = get_optimal_object_size(sizeof(upstreamsData), 1);
sizeofobj = sizeof(upstreamsData);
counter = &counters->upstreams_MAX;
break;
case DNS_CACHE:
sharedMemory = &shm_dns_cache;
allocation_step = get_optimal_object_size(sizeof(DNSCacheData), 1);
sizeofobj = sizeof(DNSCacheData);
counter = &counters->dns_cache_MAX;
break;
case STRINGS:
sharedMemory = &shm_strings;
allocation_step = STRINGS_ALLOC_STEP;
sizeofobj = 1;
counter = &counters->strings_MAX;
break;
default:
logg("Invalid argument in enlarge_shmem_struct(%i)", type);
return 0;
}
// Reallocate enough space for requested object
const size_t current = sharedMemory->size/sizeofobj;
realloc_shm(sharedMemory, current + allocation_step, sizeofobj, true);
// Add allocated memory to corresponding counter
*counter += allocation_step;
return sharedMemory->ptr;
}
static bool realloc_shm(SharedMemory *sharedMemory, const size_t size1, const size_t size2, const bool resize)
{
// Absolute target size
const size_t size = size1 * size2;
// Log that we are doing something here
char df[64] = { 0 };
const int percentage = get_dev_shm_usage(df);
// Log output
if(resize)
logg("Resizing \"%s\" from %zu to (%zu * %zu) == %zu (%s)",
sharedMemory->name, sharedMemory->size, size1, size2, size, df);
else
logg("Remapping \"%s\" from %zu to (%zu * %zu) == %zu",
sharedMemory->name, sharedMemory->size, size1, size2, size);
if(config.check.shmem > 0 && percentage > config.check.shmem)
log_resource_shortage(-1.0, 0, percentage, -1, SHMEM_PATH, df);
// Resize shard memory object if requested
// If not, we only remap a shared memory object which might have changed
// in another process. This happens when pihole-FTL forks due to incoming
// TCP requests.
if(resize)
{
// Verify shared memory ownership
verify_shmem_pid();
// Open shared memory object
const int fd = shm_open(sharedMemory->name, O_RDWR, S_IRUSR | S_IWUSR);
if(fd == -1)
{
logg("FATAL: realloc_shm(): Failed to open shared memory object \"%s\": %s",
sharedMemory->name, strerror(errno));
exit(EXIT_FAILURE);
}
// Allocate shared memory object to specified size
// Using f[tl]allocate() will ensure that there's actually space for
// this file. Otherwise we end up with a sparse file that can give
// SIGBUS if we run out of space while writing to it.
const int ret = ftlallocate(fd, 0U, size);
if(ret != 0)
{
logg("FATAL: realloc_shm(): Failed to resize \"%s\" (%i) to %zu: %s (%i)",
sharedMemory->name, fd, size, strerror(errno), ret);
exit(EXIT_FAILURE);
}
// Close shared memory object file descriptor as it is no longer
// needed after having called f[tl]allocate()
close(fd);
// Update shm counters to indicate that at least one shared memory object changed
shmSettings->global_shm_counter++;
local_shm_counter++;
}
void *new_ptr = mremap(sharedMemory->ptr, sharedMemory->size, size, MREMAP_MAYMOVE);
if(new_ptr == MAP_FAILED)
{
logg("FATAL: realloc_shm(): mremap(%p, %zu, %zu, MREMAP_MAYMOVE): Failed to reallocate \"%s\": %s",
sharedMemory->ptr, sharedMemory->size, size, sharedMemory->name,
strerror(errno));
exit(EXIT_FAILURE);
}
// Update how much memory FTL uses
// We add the difference between updated and previous size
used_shmem += (size - sharedMemory->size);
if(config.debug & DEBUG_SHMEM)
{
if(sharedMemory->ptr == new_ptr)
logg("SHMEM pointer not updated: %p (%zu %zu)",
sharedMemory->ptr, sharedMemory->size, size);
else
logg("SHMEM pointer updated: %p -> %p (%zu %zu)",
sharedMemory->ptr, new_ptr, sharedMemory->size, size);
}
sharedMemory->ptr = new_ptr;
sharedMemory->size = size;
return true;
}
static void delete_shm(SharedMemory *sharedMemory)
{
// Unmap shared memory (if mmapped)
if(sharedMemory->ptr != NULL)
{
if(munmap(sharedMemory->ptr, sharedMemory->size) != 0)
logg("delete_shm(): munmap(%p, %zu) failed: %s", sharedMemory->ptr, sharedMemory->size, strerror(errno));
}
// Now you can no longer `shm_open` the memory, and once all others
// unlink, it will be destroyed.
if(shm_unlink(sharedMemory->name) != 0)
logg("delete_shm(): shm_unlink(%s) failed: %s", sharedMemory->name, strerror(errno));
}
// Euclidean algorithm to return greatest common divisor of the numbers
static size_t __attribute__((const)) gcd(size_t a, size_t b)
{
while(b != 0)
{
size_t temp = b;
b = a % b;
a = temp;
}
return a;
}
// Function to return the optimal (minimum) size for page-aligned
// shared memory objects. This routine works by computing the LCM
// of two numbers, the pagesize and the size of a single element
// in the shared memory object
static size_t get_optimal_object_size(const size_t objsize, const size_t minsize)
{
// optsize and minsize are in units of objsize
const size_t optsize = pagesize / gcd(pagesize, objsize);
if(optsize < minsize)
{
if(config.debug & DEBUG_SHMEM)
{
logg("DEBUG: LCM(%i, %zu) == %zu < %zu",
pagesize, objsize,
optsize*objsize,
minsize*objsize);
}
// Upscale optimal size by a certain factor
// Logic of this computation:
// First part: Integer division, may cause clipping, e.g., 5/3 = 1
// Second part: Catch a possibly happened clipping event by adding
// one to the number: (5 % 3 != 0) is 1
const size_t multiplier = (minsize/optsize) + ((minsize % optsize != 0) ? 1u : 0u);
if(config.debug & DEBUG_SHMEM)
{
logg("DEBUG: Using %zu*%zu == %zu >= %zu",
multiplier, optsize*objsize,
multiplier*optsize*objsize,
minsize*objsize);
}
// As optsize ensures perfect page-alignment,
// any multiple of it will be aligned as well
return multiplier*optsize;
}
else
{
if(config.debug & DEBUG_SHMEM)
{
logg("DEBUG: LCM(%i, %zu) == %zu >= %zu",
pagesize, objsize,
optsize*objsize,
minsize*objsize);
}
// Return computed optimal size
return optsize;
}
}
// Enlarge shared memory to be able to hold at least one new record
void shm_ensure_size(void)
{
if(counters->queries >= counters->queries_MAX-1)
{
// Have to reallocate shared memory
queries = enlarge_shmem_struct(QUERIES);
if(queries == NULL)
{
logg("FATAL: Memory allocation failed! Exiting");
exit(EXIT_FAILURE);
}
}
if(counters->upstreams >= counters->upstreams_MAX-1)
{
// Have to reallocate shared memory
upstreams = enlarge_shmem_struct(UPSTREAMS);
if(upstreams == NULL)
{
logg("FATAL: Memory allocation failed! Exiting");
exit(EXIT_FAILURE);
}
}
if(counters->clients >= counters->clients_MAX-1)
{
// Have to reallocate shared memory
clients = enlarge_shmem_struct(CLIENTS);
if(clients == NULL)
{
logg("FATAL: Memory allocation failed! Exiting");
exit(EXIT_FAILURE);
}
}
if(counters->domains >= counters->domains_MAX-1)
{
// Have to reallocate shared memory
domains = enlarge_shmem_struct(DOMAINS);
if(domains == NULL)
{
logg("FATAL: Memory allocation failed! Exiting");
exit(EXIT_FAILURE);
}
}
if(counters->dns_cache_size >= counters->dns_cache_MAX-1)
{
// Have to reallocate shared memory
dns_cache = enlarge_shmem_struct(DNS_CACHE);
if(dns_cache == NULL)
{
logg("FATAL: Memory allocation failed! Exiting");
exit(EXIT_FAILURE);
}
}
if(shmSettings->next_str_pos + STRINGS_ALLOC_STEP >= shm_strings.size)
{
// Have to reallocate shared memory
if(enlarge_shmem_struct(STRINGS) == NULL)
{
logg("FATAL: Memory allocation failed! Exiting");
exit(EXIT_FAILURE);
}
}
}
void reset_per_client_regex(const int clientID)
{
const unsigned int num_regex_tot = get_num_regex(REGEX_MAX); // total number
for(unsigned int i = 0u; i < num_regex_tot; i++)
{
// Zero-initialize/reset (= false) all regex (white + black)
set_per_client_regex(clientID, i, false);
}
}
void add_per_client_regex(unsigned int clientID)
{
const unsigned int num_regex_tot = get_num_regex(REGEX_MAX); // total number
const size_t size = get_optimal_object_size(1, counters->clients * num_regex_tot);
if(size > shm_per_client_regex.size &&
realloc_shm(&shm_per_client_regex, 1, size, true))
{
reset_per_client_regex(clientID);
counters->per_client_regex_MAX = size;
}
}
bool get_per_client_regex(const int clientID, const int regexID)
{
const unsigned int num_regex_tot = get_num_regex(REGEX_MAX); // total number
const unsigned int id = clientID * num_regex_tot + regexID;
const size_t maxval = shm_per_client_regex.size / sizeof(bool);
if(id > maxval)
{
logg("ERROR: get_per_client_regex(%d, %d): Out of bounds (%d > %d * %d, shm_per_client_regex.size = %zd)!",
clientID, regexID,
id, counters->clients, num_regex_tot, maxval);
return false;
}
return ((bool*) shm_per_client_regex.ptr)[id];
}
void set_per_client_regex(const int clientID, const int regexID, const bool value)
{
const unsigned int num_regex_tot = get_num_regex(REGEX_MAX); // total number
const unsigned int id = clientID * num_regex_tot + regexID;
const size_t maxval = shm_per_client_regex.size / sizeof(bool);
if(id > maxval)
{
logg("ERROR: set_per_client_regex(%d, %d, %s): Out of bounds (%d > %d * %d, shm_per_client_regex.size = %zd)!",
clientID, regexID, value ? "true" : "false",
id, counters->clients, num_regex_tot, maxval);
return;
}
((bool*) shm_per_client_regex.ptr)[id] = value;
}
static inline bool check_range(int ID, int MAXID, const char* type, const char *func, int line, const char *file)
{
// Check bounds
if(ID < 0 || ID > MAXID)
{
logg("ERROR: Trying to access %s ID %i, but maximum is %i", type, ID, MAXID);
logg(" found in %s() (%s:%i)", func, short_path(file), line);
return false;
}
// Everything okay
return true;
}
static inline bool check_magic(int ID, bool checkMagic, unsigned char magic, const char *type, const char *func, int line, const char *file)
{
// Check magic only if requested (skipped for new entries which are uninitialized)
if(checkMagic && magic != MAGICBYTE)
{
logg("ERROR: Trying to access %s ID %i, but magic byte is %x", type, ID, magic);
logg(" found in %s() (%s:%i)", func, short_path(file), line);
return false;
}
// Everything okay
return true;
}
queriesData* _getQuery(int queryID, bool checkMagic, int line, const char *func, const char *file)
{
// This does not exist, return a NULL pointer
if(queryID == -1)
return NULL;
// We are not in a locked situation, return a NULL pointer
if(config.debug & DEBUG_LOCKS && !is_our_lock())
{
logg("ERROR: Tried to obtain query pointer without lock in %s() (%s:%i)!",
func, short_path(file), line);
generate_backtrace();
return NULL;
}
if(check_range(queryID, counters->queries_MAX, "query", func, line, file) &&
check_magic(queryID, checkMagic, queries[queryID].magic, "query", func, line, file))
return &queries[queryID];
else
return NULL;
}
clientsData* _getClient(int clientID, bool checkMagic, int line, const char *func, const char *file)
{
// This does not exist, we return a NULL pointer
if(clientID == -1)
return NULL;
// We are not in a locked situation, return a NULL pointer
if(config.debug & DEBUG_LOCKS && !is_our_lock())
{
logg("ERROR: Tried to obtain client pointer without lock in %s() (%s:%i)!",
func, short_path(file), line);
generate_backtrace();
return NULL;
}
if(check_range(clientID, counters->clients_MAX, "client", func, line, file) &&
check_magic(clientID, checkMagic, clients[clientID].magic, "client", func, line, file))
return &clients[clientID];
else
return NULL;
}
domainsData* _getDomain(int domainID, bool checkMagic, int line, const char *func, const char *file)
{
// This does not exist, we return a NULL pointer
if(domainID == -1)
return NULL;
// We are not in a locked situation, return a NULL pointer
if(config.debug & DEBUG_LOCKS && !is_our_lock())
{
logg("ERROR: Tried to obtain domain pointer without lock in %s() (%s:%i)!",
func, short_path(file), line);
generate_backtrace();
return NULL;
}
if(check_range(domainID, counters->domains_MAX, "domain", func, line, file) &&
check_magic(domainID, checkMagic, domains[domainID].magic, "domain", func, line, file))
return &domains[domainID];
else
return NULL;
}
upstreamsData* _getUpstream(int upstreamID, bool checkMagic, int line, const char *func, const char *file)
{
// This does not exist, we return a NULL pointer
if(upstreamID == -1)
return NULL;
// We are not in a locked situation, return a NULL pointer
if(config.debug & DEBUG_LOCKS && !is_our_lock())
{
logg("ERROR: Tried to obtain upstream pointer without lock in %s() (%s:%i)!",
func, short_path(file), line);
generate_backtrace();
return NULL;
}
if(check_range(upstreamID, counters->upstreams_MAX, "upstream", func, line, file) &&
check_magic(upstreamID, checkMagic, upstreams[upstreamID].magic, "upstream", func, line, file))
return &upstreams[upstreamID];
else
return NULL;
}
DNSCacheData* _getDNSCache(int cacheID, bool checkMagic, int line, const char *func, const char *file)
{
// This does not exist, we return a NULL pointer
if(cacheID == -1)
return NULL;
// We are not in a locked situation, return a NULL pointer
if(config.debug & DEBUG_LOCKS && !is_our_lock())
{
logg("ERROR: Tried to obtain cache pointer without lock in %s() (%s:%i)!",
func, short_path(file), line);
generate_backtrace();
return NULL;
}
if(check_range(cacheID, counters->dns_cache_MAX, "dns_cache", func, line, file) &&
check_magic(cacheID, checkMagic, dns_cache[cacheID].magic, "dns_cache", func, line, file))
return &dns_cache[cacheID];
else
return NULL;
}