From 759cbc3c40c17f8f0ccfd28e9985b8c87cf23919 Mon Sep 17 00:00:00 2001 From: Marc Di Luzio Date: Fri, 23 Mar 2018 13:30:25 +0000 Subject: [PATCH] Add config file parsing Checks for a gamemode.ini in /usr/share/gamemode/ (or in the cwd for debugging) Currently allows for blacklisting and whitelisting clients based on rudimentary needle-haystack executable name checks See the example/gamemode.ini file for expected syntax Using the BSD licensed inih library (with additional meson.build file) --- .gitignore | 1 + README.md | 10 ++ daemon/daemon_config.c | 245 +++++++++++++++++++++++++++++++++++++++++ daemon/daemon_config.h | 72 ++++++++++++ daemon/gamemode.c | 19 ++++ daemon/meson.build | 2 + data/gamemoded.1 | 21 ++++ example/gamemode.ini | 9 ++ meson.build | 4 + subprojects/inih.wrap | 5 + 10 files changed, 388 insertions(+) create mode 100644 daemon/daemon_config.c create mode 100644 daemon/daemon_config.h create mode 100644 example/gamemode.ini create mode 100644 subprojects/inih.wrap diff --git a/.gitignore b/.gitignore index bbbf928..70f650c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build/ +subprojects/inih/ *.swp diff --git a/README.md b/README.md index adebbd0..7b9852f 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,16 @@ Add `-lgamemodeauto` to linker arguments and distribute `libgamemodeauto.so` wit #### Option 3: Distribute and script Distribute `libgamemodeauto.so` with the game and add to LD\_PRELOAD in a launch script +--- +## Configuration + +The daemon can currently be configured using a `gamemode.ini` file in `/usr/share/gamemode/`. It will load the file when starting up. + +An example of what the file could look like is found in the `example` directory. + +The file parsing uses [inih](https://github.com/benhoyt/inih). + + --- ## Pull Requests Pull requests must match with the coding style found in the `.clang-format` file diff --git a/daemon/daemon_config.c b/daemon/daemon_config.c new file mode 100644 index 0000000..9bb6c82 --- /dev/null +++ b/daemon/daemon_config.c @@ -0,0 +1,245 @@ +/* + +Copyright (c) 2017, Feral Interactive +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * 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. + * Neither the name of Feral Interactive nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 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. + + */ +#define _GNU_SOURCE + +#include "daemon_config.h" +#include "logging.h" + +/* Ben Hoyt's inih library */ +#include "ini.h" + +#include +#include +#include +#include + +/* Name and possible location of the config file */ +#define CONFIG_NAME "gamemode.ini" +#define CONFIG_DIR "/usr/share/gamemode/" + +/* Maximum values in the whilelist and blacklist */ +#define MAX_LIST_VALUES 32 + +/* Maximum length of values in the whilelist or blacklist */ +#define MAX_LIST_VALUE_LENGTH 256 + +/** + * The config holds various details as needed + * and a rwlock to allow config_reload to be called + */ +struct GameModeConfig { + pthread_rwlock_t rwlock; + int inotfd; + int inotwd; + + char whitelist[MAX_LIST_VALUES][MAX_LIST_VALUE_LENGTH]; + char blacklist[MAX_LIST_VALUES][MAX_LIST_VALUE_LENGTH]; +}; + +/* + * Handler for the inih callback + */ +static int inih_handler(void *user, const char *section, const char *name, const char *value) +{ + GameModeConfig *self = (GameModeConfig *)user; + bool valid = false; + + /* Filter subsection */ + if (strcmp(section, "filter") == 0) { + if (strcmp(name, "whitelist") == 0) { + valid = true; + + unsigned int i = 0; + while (*self->whitelist[i] && ++i < MAX_LIST_VALUES) + ; + + if (i < MAX_LIST_VALUES) { + strncpy(self->whitelist[i], value, MAX_LIST_VALUE_LENGTH); + } else { + LOG_MSG("Could not add [%s] to the whitelist, exceeds limit of %d\n", + value, + MAX_LIST_VALUES); + } + } else if (strcmp(name, "blacklist") == 0) { + valid = true; + + unsigned int i = 0; + while (*self->blacklist[i] && ++i < MAX_LIST_VALUES) + ; + + if (i < MAX_LIST_VALUES) { + strncpy(self->blacklist[i], value, MAX_LIST_VALUE_LENGTH); + } else { + LOG_MSG("Could not add [%s] to the blacklist, exceeds limit of %d\n", + value, + MAX_LIST_VALUES); + } + } + } + + if (!valid) { + /* We hit an unknown section succeed but with a log */ + LOG_MSG("Unknown value in config file [%s] %s=%s\n", section, name, value); + } + + return 1; +} + +/* + * Load the config file + */ +static void load_config_file(GameModeConfig *self) +{ + /* Take the write lock for the internal data */ + pthread_rwlock_wrlock(&self->rwlock); + + /* Clear our config values */ + memset(self->whitelist, 0, sizeof(self->whitelist)); + memset(self->blacklist, 0, sizeof(self->blacklist)); + + /* try locally first */ + FILE *f = fopen(CONFIG_NAME, "r"); + if (!f) { + f = fopen(CONFIG_DIR CONFIG_NAME, "r"); + if (!f) { + /* Failure here isn't fatal */ + LOG_ERROR("Note: No config file found [%s] in working directory or in [%s]\n", + CONFIG_NAME, + CONFIG_DIR); + } + } + + if (f) { + int error = ini_parse_file(f, inih_handler, (void *)self); + + /* Failure here isn't fatal */ + if (error) { + LOG_MSG("Failed to parse config file - error on line %d!\n", error); + } + + fclose(f); + } + + /* Release the lock */ + pthread_rwlock_unlock(&self->rwlock); +} + +/* + * Create a context object + */ +GameModeConfig *config_create(void) +{ + GameModeConfig *newconfig = (GameModeConfig *)malloc(sizeof(GameModeConfig)); + + return newconfig; +} + +/* + * Initialise the config + */ +void config_init(GameModeConfig *self) +{ + pthread_rwlock_init(&self->rwlock, NULL); + + /* load the initial config */ + load_config_file(self); +} + +/* + * Re-load the config file + */ +void config_reload(GameModeConfig *self) +{ + load_config_file(self); +} + +/* + * Destroy the config + */ +void config_destroy(GameModeConfig *self) +{ + pthread_rwlock_destroy(&self->rwlock); + + /* Finally, free the memory */ + free(self); +} + +/* + * Checks if the client is whitelisted + */ +bool config_get_client_whitelisted(GameModeConfig *self, const char *client) +{ + /* Take the read lock for the internal data */ + pthread_rwlock_rdlock(&self->rwlock); + + /* If the whitelist is empty then everything passes */ + bool found = true; + if (self->whitelist[0][0]) { + /* + * Check if the value is found in our whitelist + * Currently is a simple strstr check, but could be modified for wildcards etc. + */ + found = false; + for (unsigned int i = 0; i < MAX_LIST_VALUES && self->whitelist[i][0]; i++) { + if (strstr(client, self->whitelist[i])) { + found = true; + } + } + } + + /* release the lock */ + pthread_rwlock_unlock(&self->rwlock); + return found; +} + +/* + * Checks if the client is blacklisted + */ +bool config_get_client_blacklisted(GameModeConfig *self, const char *client) +{ + /* Take the read lock for the internal data */ + pthread_rwlock_rdlock(&self->rwlock); + + /* + * Check if the value is found in our whitelist + * Currently is a simple strstr check, but could be modified for wildcards etc. + */ + bool found = false; + for (unsigned int i = 0; i < MAX_LIST_VALUES && self->blacklist[i][0]; i++) { + if (strstr(client, self->blacklist[i])) { + found = true; + } + } + + /* release the lock */ + pthread_rwlock_unlock(&self->rwlock); + return found; +} diff --git a/daemon/daemon_config.h b/daemon/daemon_config.h new file mode 100644 index 0000000..89048c4 --- /dev/null +++ b/daemon/daemon_config.h @@ -0,0 +1,72 @@ +/* + +Copyright (c) 2017, Feral Interactive +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * 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. + * Neither the name of Feral Interactive nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 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. + + */ +#pragma once + +#include + +/* + * Opaque config context type + */ +typedef struct GameModeConfig GameModeConfig; + +/* + * Initialise a config + */ +GameModeConfig *config_create(void); + +/* + * Initialise a config + * Must be called before using any config later config functions + */ +void config_init(GameModeConfig *self); + +/* + * Reload a config from disk + * Thread safe to call + */ +void config_reload(GameModeConfig *self); + +/* + * Destroy a config + * Invalidates the config + */ +void config_destroy(GameModeConfig *self); + +/* + * Get if the client is in the whitelist + * returns false for an empty whitelist + */ +bool config_get_client_whitelisted(GameModeConfig *self, const char *client); + +/* + * Get if the client is in the blacklist + */ +bool config_get_client_blacklisted(GameModeConfig *self, const char *client); diff --git a/daemon/gamemode.c b/daemon/gamemode.c index d77100f..9a8ec24 100644 --- a/daemon/gamemode.c +++ b/daemon/gamemode.c @@ -32,6 +32,7 @@ POSSIBILITY OF SUCH DAMAGE. #define _GNU_SOURCE #include "gamemode.h" +#include "daemon_config.h" #include "governors.h" #include "logging.h" @@ -56,6 +57,8 @@ struct GameModeContext { _Atomic int refcount; /**refcount = ATOMIC_VAR_INIT(0); + /* Initialise the config */ + self->config = config_create(); + config_init(self->config); + /* Read current governer state before setting up any message handling */ update_initial_gov_state(); LOG_MSG("governor is set to [%s]\n", get_initial_governor()); @@ -139,6 +146,9 @@ void game_mode_context_destroy(GameModeContext *self) pthread_cond_destroy(&self->reaper.condition); pthread_mutex_destroy(&self->reaper.mutex); + /* Destroy the config object */ + config_destroy(self->config); + pthread_rwlock_destroy(&self->rwlock); } @@ -253,6 +263,15 @@ bool game_mode_context_register(GameModeContext *self, pid_t client) return false; } + /* Check our blacklist and whitelist */ + if (!config_get_client_whitelisted(self->config, cl->executable)) { + LOG_MSG("Client [%s] was rejected (not in whitelist)\n", cl->executable); + return false; + } else if (config_get_client_blacklisted(self->config, cl->executable)) { + LOG_MSG("Client [%s] was rejected (in blacklist)\n", cl->executable); + return false; + } + /* Begin a write lock now to insert our new client at list start */ pthread_rwlock_wrlock(&self->rwlock); diff --git a/daemon/meson.build b/daemon/meson.build index cbc8d96..7b6790e 100644 --- a/daemon/meson.build +++ b/daemon/meson.build @@ -21,6 +21,7 @@ daemon_sources = [ 'daemonize.c', 'dbus_messaging.c', 'governors.c', + 'daemon_config.c', ] executable( @@ -30,6 +31,7 @@ executable( link_daemon_common, dep_threads, dep_systemd, + inih_dependency, ], include_directories: [ config_h_dir, diff --git a/data/gamemoded.1 b/data/gamemoded.1 index ba0fbe7..3cedf58 100644 --- a/data/gamemoded.1 +++ b/data/gamemoded.1 @@ -66,6 +66,27 @@ Atlernatively developers can define \fBGAMEMODE_AUTO\fR to mimic the behaviour o Or, distribute \fBlibgamemodeauto.so\fR and either link with \fB\-lgamemodeauto\fR or inject it as above with \fBLD\_PRELOAD\fR. +.SH CONFIG + +\fBgamemoded\fR can be configured with a \fBgamemode.ini\fR file found in \fB/usr/share/gamemode/\fR. The daemon will load the config file on start-up if it exists. + +Behaviour of the config file can be explained by presenting a commented example: + +.RS 4 +.nf +[filter] +; If "whitelist" entry has a value(s) +; gamemode will reject anything not in the whitelist +;whitelist=RiseOfTheTombRaider + +; Gamemode will always reject anything in the blacklist +blacklist=HalfLife3 + glxgears +.fi +.RE + +This config file will currently reject any games that match \fIHalfLife3\fR or \fIglxgears\fR, but can be modified to only accept \fIRiseOfTheTombRaider\fR by removing the semicolon preceding the fourth line. + .SH SEE ALSO systemd(1) diff --git a/example/gamemode.ini b/example/gamemode.ini new file mode 100644 index 0000000..37a2a26 --- /dev/null +++ b/example/gamemode.ini @@ -0,0 +1,9 @@ +[filter] +; Gamemode will always reject anything in the blacklist +blacklist=HalfLife3 + glxgears + steam + +; If "whitelist" entry has a value(s) +; gamemode will reject anything not in the whitelist +;whitelist=RiseOfTheTombRaider diff --git a/meson.build b/meson.build index 949743f..af2c278 100644 --- a/meson.build +++ b/meson.build @@ -82,6 +82,10 @@ subdir('lib') # The daemon can be disabled if necessary, allowing multilib builds of the # main library if with_daemon == true + # inih currently only needed by the daemon + inih = subproject('inih') + inih_dependency = inih.get_variable('inih_dependency') + subdir('daemon') # All installed data is currently daemon specific diff --git a/subprojects/inih.wrap b/subprojects/inih.wrap new file mode 100644 index 0000000..9fd353c --- /dev/null +++ b/subprojects/inih.wrap @@ -0,0 +1,5 @@ +[wrap-git] +directory=inih +url=https://github.com/FeralInteractive/inih.git +revision=head +